//@version=6
// ─────────────────────────────────────────────────────────────────────────────
//  CRT - HTF Turtle Soup + OB / FVG / iFVG                        (Pine Script v6)
//  Built strictly from the trader's own CRT (Candle Range Theory) notes.
//
//  WHAT IT DOES
//  1. Finds CRT ranges on the 1H, 4H, 1D, 1W and 1M candles.
//       RULE 1  A candle becomes the CRT range when price later trades beyond its high or low
//               and CLOSES BACK INSIDE (the Turtle Soup, Ts). The sweep can come in the very
//               next candle or after inside bars, and the range candle may itself sit inside
//               an older, larger candle.
//                 sweep above + close inside = bearish (-Ts) -> 100% target = CRT Low
//                 sweep below + close inside = bullish (+Ts) -> 100% target = CRT High
//       RULE 2  The CRT range then stays IN PLAY, with both its CRT High and its CRT Low on
//               the chart, until a candle of its own timeframe CLOSES outside it. Every later
//               sweep of either end that closes back inside is another Ts of the same range.
//  2. BUY / SELL signal on the entry timeframe (the trader's rule). Each CRT timeframe has its
//     own entry timeframe: 1M -> 1D, 1W -> 4H, 1D -> 1H, 4H -> 15m, 1H -> 5m (editable).
//       BUY   Price trades below the CRT Low. The last THICK bearish entry-timeframe candle
//             that pierces the CRT Low is the Order Block (OB). As soon as a candle CLOSES
//             back INSIDE the range, the BUY label is printed under that candle.
//       SELL  Price trades above the CRT High. The last thick bullish candle that pierces the
//             CRT High is the OB. As soon as a candle CLOSES back INSIDE the range, the SELL
//             label is printed.
//       (In the settings the signal can instead wait for a close beyond the OB candle.)
//       Stop loss  at the swing point: the lowest low (BUY) / highest high (SELL) of the sweep.
//       Targets    50% of the CRT range (close half) and 100% = the opposite end (close the rest).
//     No signal is given while price is only testing a CRT High / Low without having swept it
//     and closed back inside.
//     The chart must be on the entry timeframe or lower for that CRT (for a 4H CRT: 15m or
//     lower). On a higher chart the chart timeframe is used instead and the dashboard says so.
//  3. Key levels (KL): the untaken swing highs / lows of the higher timeframe that belongs to
//     the chart: 5m chart -> 1H swings, 15m chart -> 4H swings, 4H chart -> Daily swings,
//     Daily -> Weekly, Weekly -> Monthly, Monthly -> Yearly (divide-by-4 rule of the notes).
//     A Ts that takes a swing of the timeframe one step above the CRT is tagged KL.
//  4. Marks Fair Value Gaps and Inverse Fair Value Gaps on the chart timeframe.
//  5. Past setups stay on the chart (their number is a setting). The dashboard shows the CRT
//     in play and the latest signal of each timeframe.
//  6. HTF candles at the right-hand side of the chart (group 10): the last candles of the chosen
//     higher timeframes with their timeframe, time left, candle time, the open of the forming
//     candle, sweeps and - if chosen - their Fair Value Gaps and Inverse Fair Value Gaps.
//
//  The signal, its Ts label and its OB label are printed when the entry-timeframe candle
//  closes and are not moved afterwards. When the higher-timeframe candle closes, the Ts label
//  receives its tags and the trade label says whether that candle confirmed the Turtle Soup.
//
//  7. Day and week dividers, a Trend Bias table and arrow, CISD and MSS marks on the chart
//     timeframe. Master Control at the top of the settings has one switch per feature; each feature
//     also keeps its switch in its own section, and is on when both are ticked.
//
//  8. CRT sub types (group 14): the sub type of each formation (ST1 to ST6) is labelled on the
//     HTF candles while it develops; a history on the chart and a count table can be switched on.
//
//  AFTER PASTING A NEW VERSION over an older one that is already on the chart: open the
//  indicator's settings, choose Defaults > Reset settings (or remove the indicator and add it
//  again). TradingView keeps the saved settings by position, so settings saved with an older
//  version can land on the wrong inputs of a newer one.
//
//  LABEL TAGS
//     x2   second sweep of the same end of a CRT that is in play (x3 = third, ...)
//     T    the setup formed at one of the key times of the notes
//     HTF  the sweep also took the parent timeframe's range high / low
//     KL   the sweep took a key level (a swing of the timeframe one step up, or one you typed in)
//     !    a higher-timeframe CRT in the opposite direction has not reached its target yet
// ─────────────────────────────────────────────────────────────────────────────
indicator("CRT - HTF Turtle Soup + OB / FVG / iFVG", shorttitle = "CRT-TS", overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)

// ═════════════════════════════════════════════════════════════════════════════
//  CONSTANTS
// ═════════════════════════════════════════════════════════════════════════════
const string G_TF   = "Master Control"
const string G_TS   = "2 - Turtle Soup (Ts) and CRT range"
const string G_OB   = "3 - Order Block (OB)"
const string G_FVG  = "4 - Fair Value Gap (FVG)"
const string G_IFVG = "5 - Inverse Fair Value Gap (iFVG)"
const string G_TR   = "6 - Entry / Stop Loss / Take Profit"
const string G_CF   = "7 - Confirmations from the notes (tags and filters)"
const string G_DB   = "8 - Dashboard and alerts"
const string G_KL   = "9 - Key levels (KL)"
const string G_HC   = "10 - HTF candles (right-hand side)"
const string G_DIV  = "11 - Day and week dividers"
const string G_BIAS = "12 - Trend bias"
const string G_CM   = "13 - CISD and MSS"
const string G_ST   = "14 - CRT sub types"

const string ST_SOLID = "Solid"
const string ST_DASH  = "Dashed"
const string ST_DOT   = "Dotted"

const string EM_CSD = "Order Block close (BUY / SELL signal)"
const string EM_TS  = "Turtle Soup candle close (Entry 1)"
const string EM_OB  = "Order Block retest (Entry 2)"
const string EM_IFV = "iFVG retest"

const string SL_SW = "Swing point (lowest low / highest high of the sweep)"
const string SL_25 = "25% of CRT range beyond the swept level"

const string CSD_RNG  = "Back inside the CRT range"
const string CSD_BODY = "Beyond the OB candle's open (body)"
const string CSD_WICK = "Beyond the OB candle's high / low (wick)"

const string FW_HTF  = "HTF candles only"
const string FW_BOTH = "Main chart and HTF candles"
const string FW_MAIN = "Main chart only"


const string MK_FX  = "Forex / spot gold (1, 5, 9)"
const string MK_FUT = "Futures (2, 6, 10)"

const string LB_TINY = "Tiny BUY / SELL, 50%, 100%"
const string LB_FULL = "Full text (price, reward:risk, status)"

const string DB_COMP = "Compact"
const string DB_FULL = "Full details"

const string OB_LBL = "Label (OB)"
const string OB_BOX = "Box"

const string KT_AUTO = "Auto (from the chart timeframe)"

const string TM_OFF = "Off"
const string TM_TAG = "Tag only"
const string TM_FIL = "Filter (show timed setups only)"

// ═════════════════════════════════════════════════════════════════════════════
//  INPUTS
// ═════════════════════════════════════════════════════════════════════════════
// 1 - Timeframes
bool  in1H  = input.bool(true, "CRT on 1 Hour", group = G_TF, inline = "tfa")
color col1H = input.color(#00bcd4, "", group = G_TF, inline = "tfa")
bool  in4H  = input.bool(true, "CRT on 4 Hour", group = G_TF, inline = "tfb")
color col4H = input.color(#ff9800, "", group = G_TF, inline = "tfb")
bool  inD   = input.bool(true, "CRT on 1 Day", group = G_TF, inline = "tfc")
color colD  = input.color(#2962ff, "", group = G_TF, inline = "tfc")
bool  inW   = input.bool(true, "CRT on 1 Week", group = G_TF, inline = "tfd")
color colW  = input.color(#9c27b0, "", group = G_TF, inline = "tfd")
bool  inM   = input.bool(true, "CRT on 1 Month", group = G_TF, inline = "tfe", tooltip = "A timeframe not higher than the chart's is skipped. The colour is used for the newest CRT of the timeframe; earlier CRTs use the colours in group 2.")
color colM  = input.color(#e91e63, "", group = G_TF, inline = "tfe")

// 2 - Turtle Soup
bool   showTS_s     = input.bool(true, "Show CRTs and Turtle Soups (Ts)", group = G_TS, tooltip = "CRT High, CRT Low and 50% of every CRT in play, and a -Ts / +Ts label on each sweep. The lines stay until a candle of the CRT timeframe closes outside the range.")
color  tsBullCol  = input.color(#089981, "+Ts (bullish) label", group = G_TS, inline = "tsc")
color  tsBearCol  = input.color(#f23645, "-Ts (bearish) label", group = G_TS, inline = "tsc")
string tsStyleIn  = input.string(ST_SOLID, "CRT High / Low line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TS, inline = "tsl")
int    tsWidth    = input.int(2, "width", minval = 1, maxval = 5, group = G_TS, inline = "tsl")
bool   showEQ_s     = input.bool(true, "Show 50% (EQ) line of the CRT range", group = G_TS)
color  eqCol      = input.color(#787b86, "50% line", group = G_TS, inline = "eql")
string eqStyleIn  = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TS, inline = "eql")
int    eqWidth    = input.int(1, "width", minval = 1, maxval = 5, group = G_TS, inline = "eql")
bool   showC2_s     = input.bool(true, "Show 50% of candle 2 (the Turtle Soup candle)", group = G_TS, tooltip = "The midpoint of the candle that swept the range (candle 2), drawn across candles 2 and 3. Notes: in candle 3, look for a turtle soup at the 50% of candle 2.")
color  c2Col      = input.color(#ff9800, "Candle 2 50% line", group = G_TS, inline = "c2l")
string c2StyleIn  = input.string(ST_DASH, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TS, inline = "c2l")
int    c2Width    = input.int(1, "width", minval = 1, maxval = 5, group = G_TS, inline = "c2l")
bool   showActive_s = input.bool(false, "Also show the last range candle that is waiting for a sweep", group = G_TS, tooltip = "The high and low of the last candle that broke the range before it, for each enabled timeframe. These are candidates for the next CRT, not yet CRTs.")
string actStyleIn = input.string(ST_DASH, "Waiting range line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TS, inline = "acl")
int    actWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_TS, inline = "acl")
int    maxPlays   = input.int(3, "CRT ranges kept in play per timeframe", minval = 1, maxval = 6, group = G_TS, tooltip = "A CRT stays in play until a candle of its timeframe closes outside it. Beyond this number the oldest is dropped.")
int    keepSetups = input.int(10, "Past setups shown per timeframe (Ts, OB, BUY / SELL, SL, targets)", minval = 1, maxval = 50, group = G_TS, tooltip = "Previous setups (and finished CRT ranges) kept per timeframe. Open trades always stay. TradingView allows 500 labels and 500 lines per script; beyond that the oldest drawings drop first.")
string lblSizeIn  = input.string("Small", "Label size", options = ["Tiny", "Small", "Normal", "Large"], group = G_TS)

// 3 - Order Block
bool   showOB_s     = input.bool(true, "Show the Order Block (OB) of each signal", group = G_OB, tooltip = "BUY: the last thick bearish entry-timeframe candle that pierces the CRT Low. SELL: the last thick bullish one that pierces the CRT High. If none is thick, the first candle that pierced the level.")
string obMode     = input.string(OB_LBL, "Order Block shown as", options = [OB_LBL, OB_BOX], group = G_OB, tooltip = "Label: a small OB text on the candle.\nBox: a box from the Order Block candle to the signal candle.")
bool   showOBgen_s  = input.bool(false, "Also show other order blocks on the chart timeframe", group = G_OB, tooltip = "Order blocks not tied to a CRT: the last thick candle of a leg that took an old high / low, confirmed by a close through it. Used by the 'Order Block retest' entry model.")
color  obBullCol  = input.color(#089981, "OB+ (bullish)", group = G_OB, inline = "obc")
color  obBearCol  = input.color(#f23645, "OB- (bearish)", group = G_OB, inline = "obc")
string obStyleIn  = input.string(ST_SOLID, "Line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_OB, inline = "obl")
int    obWidth    = input.int(1, "width", minval = 1, maxval = 5, group = G_OB, inline = "obl")
int    obFill     = input.int(85, "Fill transparency", minval = 0, maxval = 100, group = G_OB)
float  obThick    = input.float(1.0, "Thick candle: body at least this many times the average body (0 = any candle)", minval = 0.0, step = 0.1, group = G_OB, tooltip = "1.0 = the body is at least the average body of the previous entry-timeframe candles. If no thick candle pierces the level, the first candle that pierced it is the Order Block.")
int    obThickLen = input.int(20, "Average body measured over (bars)", minval = 2, maxval = 200, group = G_OB)
string csdLevelIn = input.string(CSD_RNG, "BUY / SELL when a candle closes", options = [CSD_RNG, CSD_BODY, CSD_WICK], group = G_OB, tooltip = "Back inside the CRT range: the first candle that closes back inside after the sweep.\nBeyond the OB candle's open, or high / low: the close must also pass the Order Block candle's open, or its whole wick (later signals).")
bool   obPurge    = input.bool(true, "Other order blocks: only when the leg took an old high / low", group = G_OB, tooltip = "Notes: an OB needs a turtle soup. The leg must have traded beyond the highest high / lowest low of the lookback below.")
int    obLookback = input.int(5, "Other order blocks: old high / low lookback (bars)", minval = 1, maxval = 200, group = G_OB)
int    obMax      = input.int(4, "Other order blocks: maximum shown", minval = 1, maxval = 30, group = G_OB)

// 4 - FVG
string fvgWhere   = input.string(FW_HTF, "Show FVG and iFVG on", options = [FW_HTF, FW_BOTH, FW_MAIN], group = G_FVG, tooltip = "HTF candles only: the gaps of the HTF candles, drawn on the HTF candles (group 10); the main chart stays clean.\nMain chart only: the chart-timeframe gaps.\nThe FVG and iFVG switches still turn each kind on or off.")
bool   showFVG_s    = input.bool(true, "Show Fair Value Gaps (FVG)", group = G_FVG, tooltip = "Three-candle gap: bullish when the low of candle 3 is above the high of candle 1, bearish when the high of candle 3 is below the low of candle 1. A candle close through it turns it into an iFVG.")
color  fvgBullCol = input.color(#9598a1, "Bullish FVG", group = G_FVG, inline = "fvc")
color  fvgBearCol = input.color(#9598a1, "Bearish FVG", group = G_FVG, inline = "fvc")
string fvgStyleIn = input.string(ST_DOT, "Line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_FVG, inline = "fvl")
int    fvgWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_FVG, inline = "fvl")
int    fvgFill    = input.int(80, "Fill transparency", minval = 0, maxval = 100, group = G_FVG)
int    fvgMinTick = input.int(0, "Minimum gap size (ticks)", minval = 0, group = G_FVG)
int    fvgMax     = input.int(6, "Maximum FVGs shown", minval = 1, maxval = 30, group = G_FVG)

// 5 - iFVG
bool   showIFVG_s    = input.bool(true, "Show Inverse Fair Value Gaps (iFVG)", group = G_IFVG, tooltip = "An FVG that a candle closed through; its direction flips. It is removed when price closes back through it.")
color  ifvBullCol  = input.color(#00e676, "Bullish iFVG", group = G_IFVG, inline = "ivc")
color  ifvBearCol  = input.color(#ff5252, "Bearish iFVG", group = G_IFVG, inline = "ivc")
string ifvStyleIn  = input.string(ST_DASH, "Line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_IFVG, inline = "ivl")
int    ifvWidth    = input.int(1, "width", minval = 1, maxval = 5, group = G_IFVG, inline = "ivl")
int    ifvFill     = input.int(85, "Fill transparency", minval = 0, maxval = 100, group = G_IFVG)
int    ifvMax      = input.int(4, "Maximum iFVGs shown", minval = 1, maxval = 30, group = G_IFVG)

// 6 - Trade plan
bool   showTrade_s   = input.bool(true, "Show Entry / Stop Loss / Take Profit", group = G_TR)
string entryModel  = input.string(EM_CSD, "Entry model", options = [EM_CSD, EM_TS, EM_OB, EM_IFV], group = G_TR, tooltip = "Order Block close: your rule - after the sweep, BUY / SELL on the first entry-timeframe candle that closes back inside the range; stop at the swing point.\nTurtle Soup candle close: at the close of the higher-timeframe Ts candle.\nOrder Block retest / iFVG retest: wait for a return to the first chart-timeframe OB / iFVG formed after the sweep.")
bool   liveEntry   = input.bool(true, "Order Block close: signal as soon as the entry-timeframe candle closes", group = G_TR, tooltip = "On: BUY / SELL as soon as an entry-timeframe candle closes back inside the range.\nOff: wait until the higher-timeframe candle has closed back inside (later signal).")
string eTf1HIn     = input.timeframe("5", "Entry timeframe for a 1H CRT", group = G_TR)
string eTf4HIn     = input.timeframe("15", "Entry timeframe for a 4H CRT", group = G_TR)
string eTfDIn      = input.timeframe("60", "Entry timeframe for a 1D CRT", group = G_TR)
string eTfWIn      = input.timeframe("240", "Entry timeframe for a 1W CRT", group = G_TR)
string eTfMIn      = input.timeframe("D", "Entry timeframe for a 1M CRT", group = G_TR, tooltip = "Notes: Monthly -> Daily, Weekly -> 4H, Daily -> 1H, 4H -> 15m, 1H -> 5m. Use a chart on the entry timeframe or lower. If an entry timeframe is not lower than its CRT timeframe, or is below the chart timeframe, the chart timeframe is used (shown in the dashboard).")
string slMode      = input.string(SL_SW, "Stop loss", options = [SL_SW, SL_25], group = G_TR, tooltip = "Swing point: the lowest low (BUY) or highest high (SELL) of the sweep.\n25%: 25% of the range beyond the swept CRT High / Low.")
int    slBufTicks  = input.int(0, "Stop loss buffer (ticks)", minval = 0, group = G_TR)
bool   beAfterTP1  = input.bool(false, "Move stop to breakeven after TP1 (50%)", group = G_TR, tooltip = "Off: the stop stays at the swing point.\nOn: after the 50% target the stop line moves to the entry price.")
float  minRR       = input.float(0.0, "Minimum reward:risk to TP2 (0 = off)", minval = 0.0, step = 0.5, group = G_TR, tooltip = "Notes: at least 1:3 to the opposite end of the range. Setups below this value are labelled Ts but get no trade plan.")
int    pendCandles = input.int(3, "Cancel a waiting entry after this many HTF candles", minval = 1, maxval = 20, group = G_TR)
bool   hideDone    = input.bool(false, "Remove Entry / SL / TP from the chart when the CRT is completed", group = G_TR, tooltip = "Off: past signals stay, ending at the bar where the trade ended (their number is set in group 2).\nOn: they disappear when the trade is over, so only open trades show a plan.")
string lblMode     = input.string(LB_TINY, "Trade labels", options = [LB_TINY, LB_FULL], group = G_TR, tooltip = "Tiny: a tiny BUY / SELL label on the signal candle, targets labelled 50% and 100%, stop SL; hover for price, reward:risk and status.\nFull text: longer labels.")
bool   sellAbove   = input.bool(true, "SELL label above the candle, pointing down (off = under the candle)", group = G_TR)
color  buyCol      = input.color(#089981, "BUY label", group = G_TR, inline = "bs")
color  sellCol     = input.color(#f23645, "SELL label", group = G_TR, inline = "bs")
bool   showEnLn    = input.bool(true, "Entry line", group = G_TR, inline = "en")
color  entryCol    = input.color(#2962ff, "", group = G_TR, inline = "en")
string enStyleIn   = input.string(ST_SOLID, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TR, inline = "en")
int    enWidth     = input.int(1, "width", minval = 1, maxval = 5, group = G_TR, inline = "en")
bool   showSLLn    = input.bool(true, "Stop loss", group = G_TR, inline = "sl")
color  slCol       = input.color(#f23645, "", group = G_TR, inline = "sl")
string slStyleIn   = input.string(ST_SOLID, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TR, inline = "sl")
int    slWidth     = input.int(1, "width", minval = 1, maxval = 5, group = G_TR, inline = "sl")
bool   showT1Ln    = input.bool(true, "50% target", group = G_TR, inline = "t1")
color  tp1Col      = input.color(#4caf50, "", group = G_TR, inline = "t1")
string tp1StyleIn  = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TR, inline = "t1")
int    tp1Width    = input.int(1, "width", minval = 1, maxval = 5, group = G_TR, inline = "t1")
bool   showT2Ln    = input.bool(true, "100% target", group = G_TR, inline = "t2")
color  tp2Col      = input.color(#089981, "", group = G_TR, inline = "t2")
string tp2StyleIn  = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_TR, inline = "t2")
int    tp2Width    = input.int(1, "width", minval = 1, maxval = 5, group = G_TR, inline = "t2")

// 7 - Confirmations
string timeMode  = input.string(TM_TAG, "Timed CRT / timed Turtle Soup", options = [TM_OFF, TM_TAG, TM_FIL], group = G_CF, tooltip = "Tag: adds 'T' to the Ts label when the setup formed at a key time of the notes.\n1H / 4H: the Ts candle opens at one of the hours below.\n1D: Monday range swept Tue / Wed; Friday range swept Monday (forex); Wednesday range (futures).\n1W: the range is the first week of the month.")
string tzIn      = input.string("America/New_York", "Clock used for the key times", group = G_CF, tooltip = "Clock for the key hours below. CRTs are detected in every session; the tag only marks the timed ones.")
string mktIn     = input.string(MK_FX, "Market for the key times", options = [MK_FX, MK_FUT], group = G_CF, tooltip = "Forex / spot gold: 4H candles on 1, 5, 9; daily ranges of Monday and Friday.\nFutures: 2, 6, 10; Monday and Wednesday.")
string hrs1H     = input.string("1,3,6,9", "1H Turtle Soup hours", group = G_CF, tooltip = "Notes: look for turtle soups at 3am, 6am and 9am, plus 1am.")
string hrs4H     = input.string("1,9", "4H Turtle Soup candle hours - forex / spot gold", group = G_CF, tooltip = "Notes: 5pm CRT -> 3am TS, 9pm CRT -> 1am/3am TS (both inside the 1am 4H candle) and 5am CRT -> 9am TS.")
string hrs4HF    = input.string("2,10", "4H Turtle Soup candle hours - futures", group = G_CF, tooltip = "The same rule moved to the 2, 6, 10 candles the notes give for futures.")
bool   tagParent = input.bool(true, "Tag 'HTF' when the sweep also took the parent timeframe's range high / low", group = G_CF, tooltip = "LTF CRT within HTF CRT. Parent timeframe by the divide-by-4 rule: 1H -> 4H, 4H -> 1D, 1D -> 1W, 1W -> 1M.")
bool   tagCounter = input.bool(true, "Tag '!' when a higher-timeframe CRT in the opposite direction is still open", group = G_CF, tooltip = "Notes: never short before a higher-timeframe CRT pending target is reached (and the reverse for longs).")

// 8 - Dashboard / alerts
bool   showDash_s  = input.bool(true, "Show the dashboard", group = G_DB)
string dashMode  = input.string(DB_COMP, "Dashboard size", options = [DB_COMP, DB_FULL], group = G_DB, tooltip = "Compact: one line per timeframe.\nFull details: adds the range candle, the forming candle and the last closed candle.")
string dashTxtIn = input.string("Tiny", "Compact dashboard text size", options = ["Tiny", "Small", "Normal"], group = G_DB)
string dashPosIn = input.string("Top right", "Dashboard position", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = G_DB)
bool   showBS_s    = input.bool(true, "Show the BUY / SELL labels", group = G_TR, tooltip = "Hides only the BUY / SELL label; lines, Ts and OB labels and alerts stay.")
string bsSizeIn  = input.string("Tiny", "BUY / SELL label size", options = ["Tiny", "Small", "Normal"], group = G_TR)
bool   useAlerts_s = input.bool(true, "Send alerts (BUY / SELL signal, Turtle Soup, 50%, 100%, SL)", group = G_DB, tooltip = "Create one alert on this indicator with the condition 'Any alert() function call'.")

// 9 - Key levels
bool   showKLauto_s = input.bool(true, "Use key levels from the timeframe above (divide-by-4 rule)", group = G_KL, tooltip = "A key level is a swing high / low of a higher timeframe (a candle whose high or low is beyond both neighbours). A Ts that takes a swing of the timeframe one step above the CRT is tagged KL and the level is marked.")
bool   showKLchart_s = input.bool(true, "Show the untaken key levels for the chart timeframe", group = G_KL, tooltip = "Draws the swing highs above price and the swing lows below price that have not been taken yet, from the higher timeframe that belongs to the chart.")
string klTfIn     = input.string(KT_AUTO, "Key-level timeframe", options = [KT_AUTO, "1H", "4H", "1D", "1W", "1M", "1Y"], group = G_KL, tooltip = "Auto: chart up to 5m -> 1H swings, 15m to 1H -> 4H, 2H to 4H -> Daily, Daily -> Weekly, Weekly -> Monthly, Monthly -> Yearly.")
int    klCount    = input.int(3, "Key levels shown on each side of price", minval = 1, maxval = 10, group = G_KL)
bool   showKLcrt_s  = input.bool(false, "Also show the nearest key level for each CRT timeframe", group = G_KL, tooltip = "For every CRT timeframe, the nearest untaken swing above and below price of the timeframe one step up.")
bool   showKL_s    = input.bool(true, "Show the key levels I type in below", group = G_KL, tooltip = "Optional: up to three key levels of your own. A Turtle Soup whose wick reaches one of them is also tagged KL.")
float  kl1       = input.price(0.0, "Key level 1 (0 = off)", group = G_KL)
float  kl2       = input.price(0.0, "Key level 2 (0 = off)", group = G_KL)
float  kl3       = input.price(0.0, "Key level 3 (0 = off)", group = G_KL)
color  klCol     = input.color(#b2b5be, "KL line", group = G_KL, inline = "kll")
string klStyleIn = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_KL, inline = "kll")
int    klWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_KL, inline = "kll")

// 10 - HTF candles
bool   showHtf_s     = input.bool(true, "Show HTF candles at the right-hand side", group = G_HC, tooltip = "The last candles of the chosen higher timeframes, right of the last bar.")
bool   hc1H        = input.bool(false, "1H", group = G_HC, inline = "hctf")
bool   hc4H        = input.bool(true, "4H", group = G_HC, inline = "hctf")
bool   hcD         = input.bool(true, "1D", group = G_HC, inline = "hctf")
bool   hcW         = input.bool(false, "1W", group = G_HC, inline = "hctf")
bool   hcM         = input.bool(false, "1M", group = G_HC, inline = "hctf")
int    hcCount     = input.int(5, "Candles per timeframe (general count)", minval = 2, maxval = 20, group = G_HC, tooltip = "The last one is the candle that is still forming.")
string hcSizeIn    = input.string("Small", "Candle size", options = ["Tiny", "Small", "Medium", "Large", "Huge"], group = G_HC)
int    hcOffset    = input.int(20, "Distance from the last bar (bars)", minval = 3, maxval = 200, group = G_HC, tooltip = "Leave room for the price labels (CRT High / Low, SL, 50%, 100%, KL) that sit just right of the last bar.")
int    hcSetGap    = input.int(6, "Space between timeframes (bars)", minval = 1, maxval = 50, group = G_HC)
color  hcBullCol   = input.color(#86ad8c, "Up candle", group = G_HC, inline = "hcc")
color  hcBearCol   = input.color(#4d4d4d, "Down candle", group = G_HC, inline = "hcc")
color  hcBorderCol = input.color(#363a45, "Border", group = G_HC, inline = "hcc2")
color  hcWickCol   = input.color(#363a45, "Wick", group = G_HC, inline = "hcc2")
bool   hcShowTf    = input.bool(true, "Timeframe name above the candles", group = G_HC)
bool   hcShowLeft  = input.bool(true, "Time left in the forming candle", group = G_HC, tooltip = "Counts down to the close of the candle that is forming. It updates with every price tick; when the market is closed it shows n/a.")
bool   hcShowTime  = input.bool(true, "Candle time under each candle", group = G_HC, tooltip = "1H / 4H: opening hour. 1D: day. 1W: date the week opened. 1M: month. Clock of group 7.")
color  hcTxtCol    = input.color(#787b86, "Text", group = G_HC, inline = "hct")
string hcTxtSizeIn = input.string("Tiny", "", options = ["Tiny", "Small", "Normal", "Large"], group = G_HC, inline = "hct")
bool   hcShowOpen  = input.bool(true, "Open of the forming candle", group = G_HC, inline = "hco", tooltip = "A line at the opening price of the higher-timeframe candle that is forming, from the chart bar where it opened to the HTF candle.")
color  hcOpenCol   = input.color(#787b86, "", group = G_HC, inline = "hco")
string hcOpenStyleIn = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_HC, inline = "hco")
int    hcOpenWidth = input.int(1, "width", minval = 1, maxval = 5, group = G_HC, inline = "hco")
bool   hcShowSweep = input.bool(true, "Mark sweeps on the HTF candles", group = G_HC, tooltip = "A line from the high / low of an earlier HTF candle to the candle that traded beyond it and closed back inside. On the forming candle it shows while the sweep is happening.")
color  hcSwBearCol = input.color(#f23645, "High swept", group = G_HC, inline = "hcs")
color  hcSwBullCol = input.color(#089981, "Low swept", group = G_HC, inline = "hcs")
string hcSwStyleIn = input.string(ST_SOLID, "Sweep line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_HC, inline = "hcs2")
int    hcSwWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_HC, inline = "hcs2")
// New inputs are added below this line only, so the settings saved on a chart keep their place
bool   hcTwoRows   = input.bool(true, "Candle times on two rows (keeps them from overlapping)", group = G_HC)

// Revision 10
bool   showDayDiv_s    = input.bool(true, "Day dividers", group = G_DIV, tooltip = "A vertical line at the start of each trading day. Colour, line type and the day letters are in group 11.")
bool   showWeekDiv_s   = input.bool(true, "Week dividers", group = G_DIV, tooltip = "A vertical line at the start of each trading week. Colour and line type are in group 11.")
bool   showBiasTable_s = input.bool(true, "Trend Bias table", group = G_BIAS, tooltip = "A small table with the trend bias of each HTF candle timeframe that is switched on in group 10. Colours and position are in group 12.")
bool   showBiasArrow_s = input.bool(true, "Trend Bias arrow next to the HTF candle label", group = G_BIAS)
bool   showCISD_s      = input.bool(true, "CISD (Change in State of Delivery)", group = G_CM, tooltip = "Chart timeframe. Bullish: a close above the open of the latest run of down-close candles. Bearish: a close below the open of the latest run of up-close candles. Only a change is marked.")
bool   showMSS_s       = input.bool(true, "MSS (Market Structure Shift)", group = G_CM, tooltip = "Chart timeframe. Bullish: a close above the last swing high while structure was bearish. Bearish: a close below the last swing low while structure was bullish.")
bool   usePrevCol    = input.bool(true, "Earlier CRTs of a timeframe in their own colour", group = G_TS, tooltip = "The newest CRT of a timeframe keeps its colour (Master Control); earlier ones take the colour chosen here.")
color  prev1H        = input.color(color.new(#00bcd4, 60), "Earlier CRT: 1H", group = G_TS, inline = "pv1")
color  prev4H        = input.color(color.new(#ff9800, 60), "4H", group = G_TS, inline = "pv1")
color  prevD         = input.color(color.new(#2962ff, 60), "1D", group = G_TS, inline = "pv1")
color  prevW         = input.color(color.new(#9c27b0, 60), "1W", group = G_TS, inline = "pv1")
color  prevM         = input.color(color.new(#e91e63, 60), "1M", group = G_TS, inline = "pv1")
bool   showFvgMid    = input.bool(true, "Midpoint line of each FVG / iFVG", group = G_FVG, inline = "fvm")
string fvgMidStyleIn = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_FVG, inline = "fvm")
int    fvgMidWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_FVG, inline = "fvm")
color  dayDivCol     = input.color(color.new(#787b86, 40), "Day divider", group = G_DIV, inline = "dd")
string dayDivStyleIn = input.string(ST_DOT, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_DIV, inline = "dd")
int    dayDivWidth   = input.int(1, "width", minval = 1, maxval = 5, group = G_DIV, inline = "dd")
bool   showDayLetter = input.bool(true, "Day letters at the top (M T W T F)", group = G_DIV, inline = "dl", tooltip = "The letter is the trading day that starts at the divider, read on the clock of group 7.")
color  dayDivTxtCol  = input.color(#787b86, "", group = G_DIV, inline = "dl")
color  weekDivCol    = input.color(color.new(#787b86, 0), "Week divider", group = G_DIV, inline = "wd")
string weekDivStyleIn = input.string(ST_SOLID, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_DIV, inline = "wd")
int    weekDivWidth  = input.int(1, "width", minval = 1, maxval = 5, group = G_DIV, inline = "wd")
int    divMax        = input.int(30, "Dividers kept on the chart", minval = 1, maxval = 200, group = G_DIV)
color  biasUpCol     = input.color(#26a69a, "Bias up", group = G_BIAS, inline = "bc")
color  biasDnCol     = input.color(#ef5350, "Bias down", group = G_BIAS, inline = "bc")
string biasPosIn     = input.string("Bottom right", "Table position", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = G_BIAS)
string biasTxtIn     = input.string("Small", "Table text size", options = ["Tiny", "Small", "Normal"], group = G_BIAS, tooltip = "Bias from closed candles: close above the range = up, close below = down, wick above with a close back inside = down, wick below with a close back inside = up. Inside bars keep it.")
color  cisdBullCol   = input.color(#089981, "CISD bullish", group = G_CM, inline = "cc")
color  cisdBearCol   = input.color(#f23645, "bearish", group = G_CM, inline = "cc")
string cisdStyleIn   = input.string(ST_SOLID, "CISD line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_CM, inline = "cl")
int    cisdWidth     = input.int(1, "width", minval = 1, maxval = 5, group = G_CM, inline = "cl")
int    cisdMax       = input.int(4, "CISD marks kept on the chart", minval = 1, maxval = 50, group = G_CM)
color  mssBullCol    = input.color(#2962ff, "MSS bullish", group = G_CM, inline = "mc")
color  mssBearCol    = input.color(#e91e63, "bearish", group = G_CM, inline = "mc")
string mssStyleIn    = input.string(ST_DASH, "MSS line", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_CM, inline = "ml")
int    mssWidth      = input.int(1, "width", minval = 1, maxval = 5, group = G_CM, inline = "ml")
int    mssLen        = input.int(3, "MSS swing size (candles on each side)", minval = 1, maxval = 20, group = G_CM, tooltip = "A swing high is a candle whose high is above the highs of this many candles on each side; a swing low is the mirror image.")
int    mssMax        = input.int(4, "MSS marks kept on the chart", minval = 1, maxval = 50, group = G_CM)
int    hcSwMax       = input.int(5, "Sweeps shown per timeframe on the HTF candles (newest first)", minval = 1, maxval = 20, group = G_HC)
bool   alCISD        = input.bool(false, "Alert on CISD", group = G_DB, inline = "al2")
bool   alMSS         = input.bool(false, "Alert on MSS", group = G_DB, inline = "al2")
bool   alFVG         = input.bool(false, "Alert on a new HTF FVG", group = G_DB, inline = "al2")

// Revision 11
// Master Control: one switch per feature at the top of the settings. Each feature also keeps its switch in
// its own section. TradingView cannot link two boxes, so a feature is on when both of its boxes are ticked.
bool   showTS_m = input.bool(true, "Show CRTs and Turtle Soups (Ts)", group = G_TF, tooltip = "Master switch. A feature is on when this box and the same box in its own section are both ticked.")
bool   showEQ_m = input.bool(true, "Show 50% (EQ) line of the CRT range", group = G_TF)
bool   showC2_m = input.bool(true, "Show 50% of candle 2 (the Turtle Soup candle)", group = G_TF)
bool   showActive_m = input.bool(true, "Also show the last range candle that is waiting for a sweep", group = G_TF)
bool   showOB_m = input.bool(true, "Show the Order Block (OB) of each signal", group = G_TF)
bool   showOBgen_m = input.bool(true, "Also show other order blocks on the chart timeframe", group = G_TF)
bool   showFVG_m = input.bool(true, "Show Fair Value Gaps (FVG)", group = G_TF)
bool   showIFVG_m = input.bool(true, "Show Inverse Fair Value Gaps (iFVG)", group = G_TF)
bool   showTrade_m = input.bool(true, "Show Entry / Stop Loss / Take Profit", group = G_TF)
bool   showDash_m = input.bool(true, "Show the dashboard", group = G_TF)
bool   showBS_m = input.bool(true, "Show the BUY / SELL labels", group = G_TF)
bool   useAlerts_m = input.bool(true, "Send alerts (BUY / SELL signal, Turtle Soup, 50%, 100%, SL)", group = G_TF)
bool   showKLauto_m = input.bool(true, "Use key levels from the timeframe above (divide-by-4 rule)", group = G_TF)
bool   showKLchart_m = input.bool(true, "Show the untaken key levels for the chart timeframe", group = G_TF)
bool   showKLcrt_m = input.bool(true, "Also show the nearest key level for each CRT timeframe", group = G_TF)
bool   showKL_m = input.bool(true, "Show the key levels I type in below", group = G_TF)
bool   showHtf_m = input.bool(true, "Show HTF candles at the right-hand side", group = G_TF)
bool   showDayDiv_m = input.bool(true, "Day dividers", group = G_TF)
bool   showWeekDiv_m = input.bool(true, "Week dividers", group = G_TF)
bool   showBiasTable_m = input.bool(true, "Trend Bias table", group = G_TF)
bool   showBiasArrow_m = input.bool(true, "Trend Bias arrow next to the HTF candle label", group = G_TF)
bool   showCISD_m = input.bool(true, "CISD (Change in State of Delivery)", group = G_TF)
bool   showMSS_m = input.bool(true, "MSS (Market Structure Shift)", group = G_TF)
int    hcN1H       = input.int(0, "Candles shown (0 = the general count): 1H", minval = 0, maxval = 20, group = G_HC, inline = "hcn")
int    hcN4H       = input.int(0, "4H", minval = 0, maxval = 20, group = G_HC, inline = "hcn")
int    hcND        = input.int(0, "1D", minval = 0, maxval = 20, group = G_HC, inline = "hcn")
int    hcNW        = input.int(0, "1W", minval = 0, maxval = 20, group = G_HC, inline = "hcn2")
int    hcNM        = input.int(0, "1M", minval = 0, maxval = 20, group = G_HC, inline = "hcn2", tooltip = "The number of candles shown for each timeframe. 0 uses 'Candles per timeframe' above. The last candle is the one that is still forming.")
bool   cmClear     = input.bool(true, "Move the CISD / MSS label clear of the candles (dotted leader line)", group = G_CM, tooltip = "Moves the label to the nearest clear spot left of its line, or just right of the confirming candle, joined by a dotted line.")
int    cmReach     = input.int(15, "Furthest the label may move (candles)", minval = 2, maxval = 60, group = G_CM)

// A feature is on when its Master Control box and the box in its own section are both ticked
bool showTS = showTS_s and showTS_m
bool showEQ = showEQ_s and showEQ_m
bool showC2 = showC2_s and showC2_m
bool showActive = showActive_s and showActive_m
bool showOB = showOB_s and showOB_m
bool showOBgen = showOBgen_s and showOBgen_m
bool showFVG = showFVG_s and showFVG_m
bool showIFVG = showIFVG_s and showIFVG_m
bool showTrade = showTrade_s and showTrade_m
bool showDash = showDash_s and showDash_m
bool showBS = showBS_s and showBS_m
bool useAlerts = useAlerts_s and useAlerts_m
bool showKLauto = showKLauto_s and showKLauto_m
bool showKLchart = showKLchart_s and showKLchart_m
bool showKLcrt = showKLcrt_s and showKLcrt_m
bool showKL = showKL_s and showKL_m
bool showHtf = showHtf_s and showHtf_m
bool showDayDiv = showDayDiv_s and showDayDiv_m
bool showWeekDiv = showWeekDiv_s and showWeekDiv_m
bool showBiasTable = showBiasTable_s and showBiasTable_m
bool showBiasArrow = showBiasArrow_s and showBiasArrow_m
bool showCISD = showCISD_s and showCISD_m
bool showMSS = showMSS_s and showMSS_m

// ═════════════════════════════════════════════════════════════════════════════
//  HELPERS
// ═════════════════════════════════════════════════════════════════════════════
f_style(string s) =>
    switch s
        ST_DASH => line.style_dashed
        ST_DOT  => line.style_dotted
        => line.style_solid

f_size(string s) =>
    switch s
        "Tiny"   => size.tiny
        "Normal" => size.normal
        "Large"  => size.large
        => size.small

f_pos(string s) =>
    switch s
        "Top left"     => position.top_left
        "Bottom right" => position.bottom_right
        "Bottom left"  => position.bottom_left
        => position.top_right

f_px(float p) =>
    str.tostring(p, format.mintick)

// Shared helpers that keep the compiled script small
color NOBG = color.new(color.white, 100)

// A left-aligned table cell (an empty tooltip shows nothing)
f_cell(table t, int c, int r, string s, color tc, string sz, string tip) =>
    table.cell(t, c, r, s, text_color = tc, text_size = sz, text_halign = text.align_left, tooltip = tip)

// A text label to the right of a point in time
f_txt(int x, float y, string s, color tc, string sz, string tip) =>
    label.new(x, y, s, xloc = xloc.bar_time, color = NOBG, style = label.style_label_left, textcolor = tc, size = sz, tooltip = tip)

// A line between two points in time
f_ln(int x1, float y1, int x2, float y2, color c, string sty, int wid) =>
    line.new(x1, y1, x2, y2, xloc = xloc.bar_time, color = c, style = sty, width = wid)

f_inList(string lst, int v) =>
    str.contains("," + str.replace_all(lst, " ", "") + ",", "," + str.tostring(v) + ",")

string tsStyle  = f_style(tsStyleIn)
string eqStyle  = f_style(eqStyleIn)
string actStyle = f_style(actStyleIn)
string obStyle  = f_style(obStyleIn)
string fvgStyle = f_style(fvgStyleIn)
string ifvStyle = f_style(ifvStyleIn)
string enStyle  = f_style(enStyleIn)
string slStyle  = f_style(slStyleIn)
string tp1Style = f_style(tp1StyleIn)
string tp2Style = f_style(tp2StyleIn)
string c2Style  = f_style(c2StyleIn)
string dashTxt  = f_size(dashTxtIn)
bool   tinyLbl  = lblMode == LB_TINY
string klStyle  = f_style(klStyleIn)
string lblSize  = f_size(lblSizeIn)
bool   csdWick  = csdLevelIn == CSD_WICK
bool   csdRng   = csdLevelIn == CSD_RNG
bool   fvgOnChart = fvgWhere != FW_HTF
bool   fvgOnHtf   = fvgWhere != FW_MAIN
string hcTxt      = f_size(hcTxtSizeIn)
string hcOpenStyle = f_style(hcOpenStyleIn)
string bsSize      = bsSizeIn == "Normal" ? size.normal : bsSizeIn == "Small" ? size.small : size.tiny
string hcSwStyle   = f_style(hcSwStyleIn)
string fvgMidStyle  = f_style(fvgMidStyleIn)
string dayDivStyle  = f_style(dayDivStyleIn)
string weekDivStyle = f_style(weekDivStyleIn)
string cisdStyle    = f_style(cisdStyleIn)
string mssStyle     = f_style(mssStyleIn)
string biasTxt      = f_size(biasTxtIn)

// Right-hand anchor for price labels (three chart bars into the future)
int barMs  = timeframe.in_seconds() * 1000
int tRight = time + barMs * 3

// Revision 13 : CRT sub types
bool   showST_m      = input.bool(true, "CRT sub type on the HTF candles", group = G_TF)
bool   showSTHist_m  = input.bool(true, "CRT sub type history on the chart", group = G_TF)
bool   showSTTable_m = input.bool(true, "CRT sub type count table", group = G_TF)
bool   showST_s      = input.bool(true, "CRT sub type on the HTF candles", group = G_ST, tooltip = "A dashed outline around the candles of a formation, with its sub type (ST1 to ST6) at the top. '?' = it can still change.\nST1: sweep, delivery on candle 3. ST2: delivery on candle 4. ST3: candle 2 sweeps and delivers. ST4: inside candle(s) first. ST5: the reversal fails and price continues. ST6: two or more sweep candles.")
bool   showSTHist_s  = input.bool(false, "CRT sub type history on the chart", group = G_ST, tooltip = "Labels every finished formation on the chart at the swing point of its sweep, for the CRT timeframes switched on.")
bool   showSTTable_s = input.bool(false, "CRT sub type count table", group = G_ST, tooltip = "Formations of each sub type on the loaded history, per timeframe: formed / reached 50% / reached 100%. Hover for the number that took a key level.")
color  stBearCol     = input.color(#f23645, "High swept (bearish)", group = G_ST, inline = "stc")
color  stBullCol     = input.color(#089981, "Low swept (bullish)", group = G_ST, inline = "stc")
color  stFailCol     = input.color(#9e9e9e, "Not completed", group = G_ST, inline = "stc")
string stTxtIn       = input.string("Tiny", "History label size", options = ["Tiny", "Small", "Normal"], group = G_ST)
int    stKeep        = input.int(20, "History labels kept per timeframe", minval = 1, maxval = 80, group = G_ST)
int    stMaxAge      = input.int(6, "Close a formation as not completed after this many candles without reaching the opposite end", minval = 2, maxval = 20, group = G_ST)
string stTbPosIn     = input.string("Bottom left", "Count table position", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = G_ST)
string stTbTxtIn     = input.string("Tiny", "Count table text size", options = ["Tiny", "Small", "Normal"], group = G_ST)
bool   stGrey5       = input.bool(true, "Type 5: turn the BUY / SELL grey when price turned back before 50%", group = G_ST, inline = "stg", tooltip = "Type 5: the BUY / SELL given during candle 2 keeps its colour if price reached 50% of candle 1 before turning back; otherwise it turns grey and stays on the chart.")
color  stGreyCol     = input.color(#9e9e9e, "", group = G_ST, inline = "stg")

// Revision 15
color  stBoxCol      = input.color(#5b7ff0, "Formation outline on the HTF candles", group = G_ST, inline = "stb")
string stBoxStyleIn  = input.string(ST_DASH, "", options = [ST_SOLID, ST_DASH, ST_DOT], group = G_ST, inline = "stb")
int    stBoxWidth    = input.int(1, "width", minval = 1, maxval = 5, group = G_ST, inline = "stb")
int    stBoxMax      = input.int(1, "Formations outlined per timeframe", minval = 1, maxval = 4, group = G_ST, tooltip = "The formation that is developing is outlined first, then the latest completed ones. Outlines never overlap.")

bool showST      = showST_s and showST_m
string stBoxStyle = f_style(stBoxStyleIn)
bool showSTHist  = showSTHist_s and showSTHist_m
bool showSTTable = showSTTable_s and showSTTable_m
string stTxt     = f_size(stTxtIn)
string stTbTxt   = f_size(stTbTxtIn)

// ═════════════════════════════════════════════════════════════════════════════
//  TYPES
// ═════════════════════════════════════════════════════════════════════════════
// A swing high or swing low of a timeframe. It is a key level for the timeframes below it.
type Lvl
    float p
    int   t
    int   sweptT

// A candle range.
//   kind 0 = the last candle that broke the range before it (the waiting range, with its inside bars)
//   kind 1 = the previous candle, when that candle sits inside the waiting range
//   kind 2 = a CRT in play: a range whose high or low has been swept with a close back inside.
//            It stays in play, on both sides, until a candle of its timeframe closes outside it.
// The "h.." fields follow a sweep of the HIGH on the entry timeframe (sell side),
// the "l.." fields follow a sweep of the LOW (buy side).
type Rng
    int   id
    int   kind     = 0
    float hi
    float lo
    int   c1T
    int   c1C
    int   insideN  = 0
    int   tapsH    = 0
    int   tapsL    = 0
    int   lastDir  = 0
    int   status   = 0
    bool  used     = false
    bool  hOut     = false
    float hExt
    int   hExtT
    bool  hOb      = false
    bool  hObThick = false
    float hObO
    float hObH
    float hObL
    int   hObT
    int   hCsdT
    bool  lOut     = false
    float lExt
    int   lExtT
    bool  lOb      = false
    bool  lObThick = false
    float lObO
    float lObH
    float lObL
    int   lObT
    int   lCsdT
    line  lnH
    line  lnL
    line  lnEq
    label lbH
    label lbL
    line  klLn
    label klLb

// State of one scanned timeframe
type TFState
    string name
    int    rank
    color  col
    string parName
    bool   inited  = false
    Rng    m
    Rng    p
    bool   hasM    = false
    bool   hasP    = false
    array<Rng>   plays
    array<Rng>   done
    array<Lvl>   hiLv
    array<Lvl>   loLv
    array<float> bodies
    float  a1h
    float  a1l
    int    a1t
    float  a2h
    float  a2l
    float  crh
    float  crl
    float  oldH
    float  oldL
    int    updBar
    float  curHi
    float  curLo
    int    curHiT
    int    curLoT
    int    htfT
    int    prevHtfT
    int    c2Seen
    int    sigT
    int    freshUpT
    int    freshDnT
    int    tsUpSeen
    int    tsDnSeen
    string lastEvt = "-"
    color  pcol
    bool   eNew    = false
    float  eo
    float  eh
    float  el
    float  ec
    int    et
    bool   eThick  = false
    float  eHi
    float  eLo
    int    eHiT
    int    eLoT
    line   actH
    line   actL
    label  actHL
    label  actLL
    line   klHiLn
    line   klLoLn
    label  klHiLb
    label  klLoLb

// One signal (a sweep of a range end with a close back inside) and its trade plan
type Setup
    TFState tfs
    Rng     rg
    int     rid
    string  tfName
    int     rank
    int     dir
    float   crh
    float   crl
    float   eq
    int     tsT
    float   tsExt
    float   entry
    float   sl
    float   tp1
    float   tp2
    bool    t1ok    = true
    bool    t1hit   = false
    int     state   = 0
    int     armT
    int     age     = 0
    bool    live    = false
    int     skipBar
    string  note    = ""
    string  tags    = ""
    string  tip     = ""
    bool    hasOB   = false
    float   obO
    float   obH
    float   obL
    int     obT
    box     obBox
    label   lbOB
    line    lnEn
    line    lnSL
    line    lnT1
    line    lnT2
    label   lbTS
    label   lbEn
    label   lbSL
    label   lbT1
    label   lbT2
    line    lnC2
    label   lbC2
    bool    hasDraw = false
    int     enT

// A chart-timeframe price zone (FVG, iFVG or other order block)
type Zone
    int   dir
    float top
    float bot
    int   confT
    int   bornBar
    int   leftBar
    float cPx
    float legExt
    box   bx
    line  mid

// Setup.state values
//   0 waiting for entry   1 live   2 50% target hit   3 100% target hit (done)
//   4 stop loss           5 breakeven after the 50% target   6 cancelled   7 no trade plan

var array<Setup> setups = array.new<Setup>()
var array<Zone>  fvgs   = array.new<Zone>()
var array<Zone>  ifvgs  = array.new<Zone>()
var array<Zone>  obs    = array.new<Zone>()

// Time of the chart bar that has just closed (signals are read at the close of a candle)
int sigBarT = nz(time[1], time)

// ═════════════════════════════════════════════════════════════════════════════
//  CHART-TIMEFRAME ZONES : FVG, iFVG, OB
// ═════════════════════════════════════════════════════════════════════════════
// kind: 0 = FVG, 1 = iFVG, 2 = OB
f_addZone(array<Zone> arr, int kind, int dir, float top, float bot, int leftBar, int maxShown, float legExt) =>
    Zone z = Zone.new()
    z.dir     := dir
    z.top     := top
    z.bot     := bot
    z.confT   := time
    z.bornBar := bar_index
    z.leftBar := leftBar
    z.cPx     := close
    z.legExt  := legExt
    bool vis = kind == 0 ? (showFVG and fvgOnChart) : kind == 1 ? (showIFVG and fvgOnChart) : showOBgen
    if vis
        color  c   = kind == 0 ? (dir == 1 ? fvgBullCol : fvgBearCol) : kind == 1 ? (dir == 1 ? ifvBullCol : ifvBearCol) : (dir == 1 ? obBullCol : obBearCol)
        string sty = kind == 0 ? fvgStyle : kind == 1 ? ifvStyle : obStyle
        int    wid = kind == 0 ? fvgWidth : kind == 1 ? ifvWidth : obWidth
        int    fil = kind == 0 ? fvgFill : kind == 1 ? ifvFill : obFill
        string txt = kind == 0 ? "FVG" : kind == 1 ? "iFVG" : (dir == 1 ? "OB+" : "OB-")
        if kind != 2 and showFvgMid
            z.mid := line.new(math.max(leftBar, bar_index - 4000), (top + bot) / 2, bar_index + 1, (top + bot) / 2, color = c, style = fvgMidStyle, width = fvgMidWidth)
        z.bx := box.new(math.max(leftBar, bar_index - 4000), top, bar_index + 1, bot, border_color = c, border_width = wid, border_style = sty, bgcolor = color.new(c, fil), text = txt, text_size = size.tiny, text_color = c, text_halign = text.align_right, text_valign = text.align_center)
    arr.push(z)
    // keep only the newest 'maxShown' boxes on the chart, and at most 40 zones in memory
    int n = arr.size()
    if n > maxShown
        Zone hide = arr.get(n - maxShown - 1)
        box.delete(hide.bx)
        line.delete(hide.mid)
        hide.bx := na
        hide.mid := na
    if n > 40
        Zone old = arr.shift()
        box.delete(old.bx)
        line.delete(old.mid)
    true

// First zone confirmed at or after 'fromT' in the trade direction.
// mode 0 = Order Block retest, 1 = iFVG retest (the entry edge must lie between lo and hi)
f_findZone(int mode, int dir, int fromT, float lo, float hi) =>
    array<Zone> arr = obs
    if mode == 1
        arr := ifvgs
    float lvl = na
    int   cT  = na
    float px  = na
    if arr.size() > 0
        for i = 0 to arr.size() - 1
            Zone z = arr.get(i)
            float edge = dir == -1 ? z.bot : z.top
            if z.dir == dir and z.confT >= fromT and edge > lo and edge < hi
                lvl := edge
                cT  := z.confT
                px  := z.cPx
                break
    [lvl, cT, px]

// Old high / old low reference for the Order Block rule
float hh = ta.highest(high, obLookback)
float ll = ta.lowest(low, obLookback)

// Thick candle test (trader's rule: only a thick candle forms the Order Block)
float bodySize = math.abs(close - open)
float avgBody  = ta.sma(bodySize, obThickLen)
bool  isThick  = obThick <= 0 or bodySize >= obThick * avgBody[1]

// Set on the bar where a new Order Block is confirmed (used by the live entry)
bool  newBullOB  = false
bool  newBearOB  = false
float bullLegBot = na
float bearLegTop = na

// Last thick up-close candle of the current up leg (candidate bearish OB)
var float upO     = na
var float upH     = na
var float upL     = na
var int   upB     = na
var float upPrior = na
var float upTop   = na
// Last thick down-close candle of the current down leg (candidate bullish OB)
var float dnO     = na
var float dnL     = na
var float dnH     = na
var int   dnB     = na
var float dnPrior = na
var float dnBot   = na

if barstate.isconfirmed and bar_index > 2
    // ── existing FVGs: extend, or invert into an iFVG when a candle closes through
    if fvgs.size() > 0
        int i = fvgs.size() - 1
        while i >= 0
            Zone z = fvgs.get(i)
            bool closedThrough = z.dir == 1 ? close < z.bot : close > z.top
            if closedThrough
                fvgs.remove(i)
                box.delete(z.bx)
                line.delete(z.mid)
                f_addZone(ifvgs, 1, -z.dir, z.top, z.bot, z.leftBar, ifvMax, na)
            else
                box.set_right(z.bx, bar_index + 1)
                line.set_x2(z.mid, bar_index + 1)
            i -= 1
    // ── existing iFVGs: extend, or remove when price closes back through
    if ifvgs.size() > 0
        int i = ifvgs.size() - 1
        while i >= 0
            Zone z = ifvgs.get(i)
            bool failed = z.bornBar < bar_index and (z.dir == -1 ? close > z.top : close < z.bot)
            if failed
                ifvgs.remove(i)
                box.delete(z.bx)
                line.delete(z.mid)
            else
                box.set_right(z.bx, bar_index + 1)
                line.set_x2(z.mid, bar_index + 1)
            i -= 1
    // ── existing OBs: extend, or remove when price closes beyond the candle's extreme
    if obs.size() > 0
        int i = obs.size() - 1
        while i >= 0
            Zone z = obs.get(i)
            bool failed = z.bornBar < bar_index and (z.dir == -1 ? close > z.top : close < z.bot)
            if failed
                obs.remove(i)
                box.delete(z.bx)
                line.delete(z.mid)
            else
                box.set_right(z.bx, bar_index + 1)
                line.set_x2(z.mid, bar_index + 1)
            i -= 1
    // ── new FVG (three-candle gap)
    float minGap = fvgMinTick * syminfo.mintick
    if low > high[2] and low - high[2] >= minGap
        f_addZone(fvgs, 0, 1, low, high[2], bar_index - 2, fvgMax, na)
    if high < low[2] and low[2] - high >= minGap
        f_addZone(fvgs, 0, -1, low[2], high, bar_index - 2, fvgMax, na)
    // ── new bearish OB: a close below the last thick up-close candle (CSD)
    if not na(upO)
        upTop := math.max(upTop, high)
        if close < (csdWick ? upL : upO)
            if not obPurge or upTop > upPrior
                f_addZone(obs, 2, -1, upH, upO, upB, obMax, upTop)
                newBearOB  := true
                bearLegTop := upTop
            upO := na
    // ── new bullish OB: a close above the last thick down-close candle (CSD)
    if not na(dnO)
        dnBot := math.min(dnBot, low)
        if close > (csdWick ? dnH : dnO)
            if not obPurge or dnBot < dnPrior
                f_addZone(obs, 2, 1, dnO, dnL, dnB, obMax, dnBot)
                newBullOB  := true
                bullLegBot := dnBot
            dnO := na
    // ── track the last thick up-close / down-close candle of the running leg
    if close > open and isThick
        if na(upO)
            upPrior := hh[1]
            upTop   := high
        upO := open
        upH := high
        upL := low
        upB := bar_index
    if close < open and isThick
        if na(dnO)
            dnPrior := ll[1]
            dnBot   := low
        dnO := open
        dnL := low
        dnH := high
        dnB := bar_index

// ═════════════════════════════════════════════════════════════════════════════
//  SETUP (SIGNAL + TRADE PLAN) METHODS
// ═════════════════════════════════════════════════════════════════════════════
method wipeTrade(Setup s) =>
    line.delete(s.lnEn)
    line.delete(s.lnSL)
    line.delete(s.lnT1)
    line.delete(s.lnT2)
    label.delete(s.lbEn)
    label.delete(s.lbSL)
    label.delete(s.lbT1)
    label.delete(s.lbT2)
    s.lnEn := na
    s.lnSL := na
    s.lnT1 := na
    s.lnT2 := na
    s.lbEn := na
    s.lbSL := na
    s.lbT1 := na
    s.lbT2 := na
    s.hasDraw := false
    true

method wipeSetup(Setup s) =>
    s.wipeTrade()
    label.delete(s.lbTS)
    line.delete(s.lnC2)
    label.delete(s.lbC2)
    box.delete(s.obBox)
    label.delete(s.lbOB)
    true

method statusText(Setup s) =>
    switch s.state
        0 => "waiting for entry"
        1 => "live"
        2 => beAfterTP1 ? "50% target hit, half closed, stop at breakeven" : "50% target hit, half closed"
        3 => "100% target hit, closed"
        4 => "stop loss hit"
        5 => "stopped at breakeven after the 50% target"
        6 => "cancelled"
        => "no trade plan"

method statusShort(Setup s) =>
    switch s.state
        0 => "waiting"
        1 => "live"
        2 => "50% done"
        3 => "100% done"
        4 => "SL"
        5 => "BE"
        6 => "cancelled"
        => "-"

method entryText(Setup s) =>
    float risk = math.max(math.abs(s.entry - s.sl), syminfo.mintick)
    float rr   = math.abs(s.tp2 - s.entry) / risk
    s.tfName + (s.dir == -1 ? " SELL " : " BUY ") + f_px(s.entry) + "  RR 1:" + str.tostring(rr, "#.#") + "  [" + s.statusText() + "]" + (s.note == "" ? "" : "  " + s.note)

// Everything about the trade, shown when hovering over a tiny label
method tipText(Setup s) =>
    s.entryText() + "\nEntry " + f_px(s.entry) + "\nSL " + f_px(s.sl) + "\n50% target " + f_px(s.tp1) + (s.t1ok ? " (close half)" : " (already passed at entry)") + "\n100% target " + f_px(s.tp2) + " (close the rest)"

// Entry label text or tooltip after a change of status
method refreshEntry(Setup s) =>
    if tinyLbl
        label.set_tooltip(s.lbEn, s.tipText())
    else
        label.set_text(s.lbEn, s.entryText())
    true

// Trade plan drawings. t0 = time of the candle that gave the signal.
method drawTrade(Setup s, int t0) =>
    if showTrade and not na(s.entry)
        color noBg = NOBG
        s.hasDraw := true
        s.enT     := t0
        if showEnLn
            s.lnEn := f_ln(t0, s.entry, tRight, s.entry, entryCol, enStyle, enWidth)
        if showSLLn
            s.lnSL := f_ln(t0, s.sl, tRight, s.sl, slCol, slStyle, slWidth)
        if showT1Ln
            s.lnT1 := f_ln(t0, s.tp1, tRight, s.tp1, tp1Col, tp1Style, tp1Width)
        if showT2Ln
            s.lnT2 := f_ln(t0, s.tp2, tRight, s.tp2, tp2Col, tp2Style, tp2Width)
        if tinyLbl
            // tiny BUY / SELL under (or over) the signal candle, clear of the candle itself;
            // SL, 50% and 100% at the right end of their lines
            bool above = s.dir == -1 and sellAbove
            if showBS
                s.lbEn := label.new(t0, s.entry, s.dir == 1 ? "BUY" : "SELL", xloc = xloc.bar_time, yloc = above ? yloc.abovebar : yloc.belowbar, color = s.dir == 1 ? buyCol : sellCol, style = above ? label.style_label_down : label.style_label_up, textcolor = color.white, size = bsSize, tooltip = s.tipText())
            if showSLLn
                s.lbSL := f_txt(tRight, s.sl, "SL", slCol, size.tiny, s.tfName + " stop loss at the swing point " + f_px(s.sl))
            if showT1Ln
                s.lbT1 := f_txt(tRight, s.tp1, "50%", tp1Col, size.tiny, s.tfName + " 50% of the CRT range " + f_px(s.tp1) + (s.t1ok ? ", close half" : ", already passed at entry"))
            if showT2Ln
                s.lbT2 := f_txt(tRight, s.tp2, "100%", tp2Col, size.tiny, s.tfName + " 100% of the CRT range " + f_px(s.tp2) + ", close the rest")
        else
            if showBS
                s.lbEn := f_txt(tRight, s.entry, s.entryText(), entryCol, lblSize, "")
            if showSLLn
                s.lbSL := f_txt(tRight, s.sl, s.tfName + " SL " + f_px(s.sl), slCol, lblSize, "")
            if showT1Ln
                s.lbT1 := f_txt(tRight, s.tp1, s.tfName + " TP1 50% " + f_px(s.tp1) + (s.t1ok ? "  close half" : "  already passed"), tp1Col, lblSize, "")
            if showT2Ln
                s.lbT2 := f_txt(tRight, s.tp2, s.tfName + " TP2 100% " + f_px(s.tp2) + "  close rest", tp2Col, lblSize, "")
    true

// Order Block of a signal: a small "OB" label on the candle (default) or a box from the candle to the signal
method drawOB(Setup s, int t0) =>
    if showOB and s.hasOB
        color c = s.dir == 1 ? obBullCol : obBearCol
        if obMode == OB_LBL
            // kept on the side of the candle away from the Ts and BUY / SELL labels
            s.lbOB := label.new(s.obT, s.obO, "OB", xloc = xloc.bar_time, yloc = s.dir == 1 ? yloc.abovebar : yloc.belowbar, color = NOBG, style = label.style_none, textcolor = c, size = size.tiny, tooltip = s.tfName + " Order Block: the candle that pierced the CRT " + (s.dir == 1 ? "Low" : "High") + ". Open " + f_px(s.obO) + ", high " + f_px(s.obH) + ", low " + f_px(s.obL))
        else
            float top = s.dir == 1 ? (csdWick ? s.obH : s.obO) : s.obH
            float bot = s.dir == 1 ? s.obL : (csdWick ? s.obL : s.obO)
            s.obBox := box.new(s.obT, top, t0, bot, xloc = xloc.bar_time, border_color = c, border_width = obWidth, border_style = obStyle, bgcolor = color.new(c, obFill), text = "OB " + s.tfName, text_size = size.small, text_color = c, text_halign = text.align_right, text_valign = text.align_center)
    true

// Runs once per chart bar for every stored setup. Returns an event code:
// 0 none, 1 entry, 2 50% target, 3 100% target, 4 SL, 5 breakeven, 6 cancelled
method update(Setup s) =>
    int ev = 0
    if s.state == 0
        // only the "Order Block retest" and "iFVG retest" entry models wait for an entry
        bool broke = s.dir == -1 ? high > s.tsExt : low < s.tsExt
        bool gone  = s.dir == -1 ? low <= s.tp2 : high >= s.tp2
        bool over  = s.rg.kind == 2 and s.rg.status != 0
        if broke or gone or over or s.age > pendCandles
            s.state := 6
            ev := 6
        else
            if na(s.entry)
                float lo = math.min(s.sl, s.tp2)
                float hi = math.max(s.sl, s.tp2)
                int mode = entryModel == EM_OB ? 0 : 1
                [lvl, cT, px] = f_findZone(mode, s.dir, s.tsT, lo, hi)
                if not na(lvl)
                    float rr = math.abs(s.tp2 - lvl) / math.max(math.abs(lvl - s.sl), syminfo.mintick)
                    if minRR > 0 and rr < minRR
                        s.state := 6
                        ev := 6
                    else
                        s.entry := lvl
                        s.armT  := cT
                        s.t1ok  := s.dir == -1 ? lvl > s.tp1 : lvl < s.tp1
                        s.drawTrade(time)
            else if time > s.armT
                bool trig = s.dir == -1 ? high >= s.entry : low <= s.entry
                if trig
                    s.state := 1
                    ev := 1
    else if (s.state == 1 or s.state == 2) and (na(s.skipBar) or bar_index > s.skipBar)
        bool  atBE  = s.state == 2 and beAfterTP1
        float slNow = atBE ? s.entry : s.sl
        bool  hitSL = s.dir == -1 ? high >= slNow : low <= slNow
        bool  hitT1 = s.dir == -1 ? low <= s.tp1 : high >= s.tp1
        bool  hitT2 = s.dir == -1 ? low <= s.tp2 : high >= s.tp2
        if hitSL
            ev := atBE ? 5 : 4
            s.state := ev
        else if hitT2
            s.state := 3
            ev := 3
        else if hitT1 and s.state == 1 and s.t1ok
            s.state := 2
            ev := 2
    // drawings: follow price while the trade is open; when it is over, remove them or freeze them
    if s.hasDraw
        if s.state == 6 or (hideDone and s.state >= 3)
            s.wipeTrade()
        else
            if s.state <= 2
                line.set_x2(s.lnEn, tRight)
                line.set_x2(s.lnSL, tRight)
                line.set_x2(s.lnT1, tRight)
                line.set_x2(s.lnT2, tRight)
                if not tinyLbl
                    label.set_x(s.lbEn, tRight)
                label.set_x(s.lbSL, tRight)
                label.set_x(s.lbT1, tRight)
                label.set_x(s.lbT2, tRight)
                if s.state == 2 and beAfterTP1
                    line.set_y1(s.lnSL, s.entry)
                    line.set_y2(s.lnSL, s.entry)
                    label.set_y(s.lbSL, s.entry)
                    if tinyLbl
                        label.set_text(s.lbSL, "SL (BE)")
                        label.set_tooltip(s.lbSL, s.tfName + " stop loss moved to breakeven " + f_px(s.entry))
                    else
                        label.set_text(s.lbSL, s.tfName + " SL at breakeven " + f_px(s.entry))
            if ev > 0
                s.refreshEntry()
                if ev >= 3
                    line.set_x2(s.lnEn, time)
                    line.set_x2(s.lnSL, time)
                    line.set_x2(s.lnT1, time)
                    line.set_x2(s.lnT2, time)
                    if not tinyLbl
                        label.set_x(s.lbEn, time)
                    label.set_x(s.lbSL, time)
                    label.set_x(s.lbT1, time)
                    label.set_x(s.lbT2, time)
    ev

// Is a higher-timeframe trade in the opposite direction still open?
f_counterHTF(int rank, int dir) =>
    bool res = false
    if setups.size() > 0
        for i = 0 to setups.size() - 1
            Setup o = setups.get(i)
            if o.rank > rank and o.dir == -dir and o.state <= 2
                res := true
                break
    res

// Is there a trade in this direction on this range that is still waiting or live?
f_openSetup(int rid, int dir) =>
    bool res = false
    if setups.size() > 0
        for i = 0 to setups.size() - 1
            Setup o = setups.get(i)
            if o.rid == rid and o.dir == dir and o.state <= 2
                res := true
                break
    res

// Timed CRT / timed Turtle Soup rules of the notes
f_isTimed(string tfName, int c1Close, int tsOpen, int tsClose) =>
    bool r = false
    if tfName == "1H"
        r := f_inList(hrs1H, hour(tsOpen, tzIn))
    else if tfName == "4H"
        r := f_inList(mktIn == MK_FUT ? hrs4HF : hrs4H, hour(tsOpen, tzIn))
    else if tfName == "1D"
        int d1 = dayofweek(c1Close - 1, tzIn)
        int d2 = dayofweek(tsClose - 1, tzIn)
        bool mon = d1 == dayofweek.monday and (d2 == dayofweek.tuesday or d2 == dayofweek.wednesday)
        bool fri = mktIn == MK_FX and d1 == dayofweek.friday and d2 == dayofweek.monday
        bool wed = mktIn == MK_FUT and d1 == dayofweek.wednesday
        r := mon or fri or wed
    else if tfName == "1W"
        r := dayofmonth(c1Close - 1, tzIn) <= 7
    r

// Did the sweep reach one of the key levels typed in by the trader? (the level lies between the range end and the wick)
f_klHit(float kl, int dir, float ext, float bnd) =>
    kl > 0 and (dir == 1 ? (ext <= kl and kl <= bnd) : (ext >= kl and kl >= bnd))

f_atKL(int dir, float ext, float bnd) =>
    showKL and (f_klHit(kl1, dir, ext, bnd) or f_klHit(kl2, dir, ext, bnd) or f_klHit(kl3, dir, ext, bnd))

// ═════════════════════════════════════════════════════════════════════════════
//  RANGES, KEY LEVELS AND CRTs IN PLAY
// ═════════════════════════════════════════════════════════════════════════════
method ensure(TFState st) =>
    if not st.inited
        st.plays  := array.new<Rng>()
        st.done   := array.new<Rng>()
        st.hiLv   := array.new<Lvl>()
        st.loLv   := array.new<Lvl>()
        st.bodies := array.new<float>()
        st.inited := true
    true

f_rng(int rank, int kind, float h, float l, int t, int tc) =>
    Rng r = Rng.new()
    r.id   := bar_index * 100 + kind * 10 + rank
    r.kind := kind
    r.hi   := h
    r.lo   := l
    r.c1T  := t
    r.c1C  := tc
    r

method wipeRng(Rng r) =>
    line.delete(r.lnH)
    line.delete(r.lnL)
    line.delete(r.lnEq)
    label.delete(r.lbH)
    label.delete(r.lbL)
    line.delete(r.klLn)
    label.delete(r.klLb)
    true

// CRT High, CRT Low and 50% of a CRT in play. The lines run from candle 1 and follow price while the CRT is in play.
method drawPlay(Rng r, TFState st) =>
    if showTS
        color noBg = NOBG
        r.lnH := f_ln(r.c1T, r.hi, tRight, r.hi, st.col, tsStyle, tsWidth)
        r.lnL := f_ln(r.c1T, r.lo, tRight, r.lo, st.col, tsStyle, tsWidth)
        if showEQ
            r.lnEq := f_ln(r.c1T, (r.hi + r.lo) / 2, tRight, (r.hi + r.lo) / 2, eqCol, eqStyle, eqWidth)
        r.lbH := f_txt(tRight, r.hi, st.name + " CRT High", st.col, lblSize, "")
        r.lbL := f_txt(tRight, r.lo, st.name + " CRT Low", st.col, lblSize, "")
    true

// A CRT range leaves play (a candle closed outside it, or newer ranges replaced it): its lines stop here
method retire(TFState st, int i) =>
    Rng r = st.plays.get(i)
    st.plays.remove(i)
    if r.status == 0
        r.status := 2
    line.set_x2(r.lnH, time)
    line.set_x2(r.lnL, time)
    line.set_x2(r.lnEq, time)
    label.delete(r.lbH)
    label.delete(r.lbL)
    r.lbH := na
    r.lbL := na
    st.done.push(r)
    if st.done.size() > keepSetups
        Rng old = st.done.shift()
        old.wipeRng()
    true

// The newest CRT of a timeframe keeps the timeframe's colour; older CRTs (still in play or finished) take the
// "previous CRT" colour
method recolor(TFState st) =>
    int n = st.plays.size()
    if n > 0
        for i = 0 to n - 1
            Rng   r = st.plays.get(i)
            color c = i == n - 1 ? st.col : st.pcol
            line.set_color(r.lnH, c)
            line.set_color(r.lnL, c)
            label.set_textcolor(r.lbH, c)
            label.set_textcolor(r.lbL, c)
    if st.done.size() > 0
        for i = 0 to st.done.size() - 1
            Rng r = st.done.get(i)
            line.set_color(r.lnH, st.pcol)
            line.set_color(r.lnL, st.pcol)
    true

// Swing highs and lows of this timeframe (three-candle swing: the middle candle is higher / lower than both
// neighbours). They are the key levels for the timeframes below (divide-by-4 rule of the notes).
method swings(TFState st, bool ok, float h1, float l1, int t1, bool isNew) =>
    st.ensure()
    if ok
        // mark the levels that price has taken
        if st.hiLv.size() > 0
            for i = 0 to st.hiLv.size() - 1
                Lvl a = st.hiLv.get(i)
                if na(a.sweptT) and high > a.p
                    a.sweptT := time
        if st.loLv.size() > 0
            for i = 0 to st.loLv.size() - 1
                Lvl b = st.loLv.get(i)
                if na(b.sweptT) and low < b.p
                    b.sweptT := time
        if isNew and not na(h1) and not na(l1)
            // forget levels that were taken before the candle that has just closed
            if st.hiLv.size() > 0
                int i = st.hiLv.size() - 1
                while i >= 0
                    Lvl a = st.hiLv.get(i)
                    if not na(a.sweptT) and a.sweptT < t1
                        st.hiLv.remove(i)
                    i -= 1
            if st.loLv.size() > 0
                int i = st.loLv.size() - 1
                while i >= 0
                    Lvl b = st.loLv.get(i)
                    if not na(b.sweptT) and b.sweptT < t1
                        st.loLv.remove(i)
                    i -= 1
            // the candle before the one that has just closed is a swing when both neighbours are lower / higher
            if not na(st.a2h) and not na(st.a1h)
                if st.a1h > st.a2h and st.a1h > h1
                    Lvl n = Lvl.new(st.a1h, st.a1t)
                    if high > n.p
                        n.sweptT := time
                    st.hiLv.push(n)
                    if st.hiLv.size() > 30
                        st.hiLv.shift()
                if st.a1l < st.a2l and st.a1l < l1
                    Lvl n = Lvl.new(st.a1l, st.a1t)
                    if low < n.p
                        n.sweptT := time
                    st.loLv.push(n)
                    if st.loLv.size() > 30
                        st.loLv.shift()
            st.a2h := st.a1h
            st.a2l := st.a1l
            st.a1h := h1
            st.a1l := l1
            st.a1t := t1
    true

// Key level taken by a sweep: a swing of the parent timeframe that lies between the CRT boundary and the
// wick, and that was first taken during the Turtle Soup candle (which opened at fromT)
f_klFind(TFState par, int dir, float ext, float bnd, int fromT) =>
    float p = na
    int   t = na
    if showKLauto and par.inited
        array<Lvl> arr = par.hiLv
        if dir == 1
            arr := par.loLv
        if arr.size() > 0
            for i = arr.size() - 1 to 0
                Lvl a = arr.get(i)
                bool between = dir == -1 ? (a.p >= bnd and a.p <= ext) : (a.p <= bnd and a.p >= ext)
                if between and not na(a.sweptT) and a.sweptT >= fromT
                    p := a.p
                    t := a.t
                    break
    [p, t]

// Nearest untaken swing of a timeframe beyond 'ref': above it (side = 1) or below it (side = -1)
f_klNext(TFState src, int side, float ref) =>
    float p = na
    int   t = na
    if src.inited
        array<Lvl> arr = src.hiLv
        if side == -1
            arr := src.loLv
        if arr.size() > 0
            for i = 0 to arr.size() - 1
                Lvl a = arr.get(i)
                if na(a.sweptT)
                    bool ok = side == 1 ? a.p > ref : a.p < ref
                    bool nearer = na(p) or (side == 1 ? a.p < p : a.p > p)
                    if ok and nearer
                        p := a.p
                        t := a.t
    [p, t]

// "+Ts 4H" / "-Ts 4H" on the wick that swept the range
f_tsText(string tfName, int dir, string tags) =>
    (dir == -1 ? "-Ts " : "+Ts ") + tfName + tags

method drawTsLabel(Setup s, string tfName, int tsT, float ext, string tags, string tip) =>
    if showTS
        // one label per wick: a second range of the same timeframe swept by the same wick shares it
        bool dup = false
        if setups.size() > 0
            for i = 0 to setups.size() - 1
                Setup o = setups.get(i)
                if o.tfName == tfName and o.dir == s.dir and o.tsT == tsT and o.tsExt == ext and not na(o.lbTS)
                    dup := true
                    break
        if not dup
            s.lbTS := label.new(tsT, ext, f_tsText(tfName, s.dir, tags), xloc = xloc.bar_time, yloc = yloc.price, color = s.dir == -1 ? tsBearCol : tsBullCol, style = s.dir == -1 ? label.style_label_lower_right : label.style_label_upper_right, textcolor = color.white, size = lblSize, tooltip = tip)
    true

// 50% of candle 2 (the Turtle Soup candle of the higher timeframe). The line runs across candle 2 and candle 3.
method drawC2(Setup s, string tfName, float c2Mid, int c2T, int c2C) =>
    if showTS and showC2 and not na(c2Mid) and not na(c2T) and not na(c2C)
        int c2End = c2C + (c2C - c2T)
        s.lnC2 := f_ln(c2T, c2Mid, c2End, c2Mid, c2Col, c2Style, c2Width)
        s.lbC2 := f_txt(c2End, c2Mid, tfName + " C2 50%", c2Col, size.tiny, tfName + " candle 2 (Turtle Soup candle) 50% level " + f_px(c2Mid))
    true

// Creates a setup on range r.
//   isLive = true  : the signal on the entry timeframe - price swept the range end and a candle has closed
//                    back through the Order Block, inside the range. t0 = time of that candle.
//   isLive = false : the higher-timeframe candle has closed as a Turtle Soup and no signal was taken yet
// Returns true when a setup was stored.
f_newSetup(TFState st, Rng r, int dir, float ext, float px, int tsT, int t0, string tags, string tip, bool isLive, float c2Mid, int c2T, int c2C) =>
    float rH  = r.hi
    float rL  = r.lo
    float rng = rH - rL
    float buf = slBufTicks * syminfo.mintick
    Setup s = Setup.new()
    s.tfs    := st
    s.rg     := r
    s.rid    := r.id
    s.tfName := st.name
    s.rank   := st.rank
    s.dir    := dir
    s.crh    := rH
    s.crl    := rL
    s.eq     := (rH + rL) / 2
    s.tsT    := tsT
    s.tsExt  := ext
    s.tp1    := s.eq
    s.tp2    := dir == -1 ? rL : rH
    // stop loss at the swing point: the highest high / lowest low of the sweep
    if slMode == SL_SW
        s.sl := dir == -1 ? ext + buf : ext - buf
    else
        s.sl := dir == -1 ? rH + 0.25 * rng + buf : rL - 0.25 * rng - buf
    float rr   = math.abs(s.tp2 - px) / math.max(math.abs(px - s.sl), syminfo.mintick)
    bool  rrOk = minRR <= 0 or rr >= minRR
    bool  keep = true
    // the Order Block of the sweep
    if dir == 1 and r.lOb
        s.hasOB := true
        s.obO   := r.lObO
        s.obH   := r.lObH
        s.obL   := r.lObL
        s.obT   := r.lObT
    if dir == -1 and r.hOb
        s.hasOB := true
        s.obO   := r.hObO
        s.obH   := r.hObH
        s.obL   := r.hObL
        s.obT   := r.hObT
    if isLive
        if rrOk and math.abs(px - s.sl) > 0
            s.entry   := px
            s.t1ok    := dir == -1 ? px > s.tp1 : px < s.tp1
            s.state   := 1
            s.live    := true
            s.note    := "(HTF candle still open)"
            // the same sweep may already carry a Ts label from the close of the higher-timeframe candle:
            // the signal takes that label over, so the wick is labelled once
            string tg0 = ""
            string tp0 = st.name + (dir == -1 ? " sweep of the CRT High " : " sweep of the CRT Low ") + f_px(dir == -1 ? rH : rL) + ", price closed back inside the range.\nThe " + st.name + " candle is still open."
            if setups.size() > 0
                int j = setups.size() - 1
                while j >= 0
                    Setup o = setups.get(j)
                    if o.tfName == st.name and o.dir == dir and o.state == 7 and o.tsT == tsT and o.tsExt == ext
                        tg0 := o.tags
                        tp0 := o.tip
                        s.note := "(Ts confirmed)"
                        s.live := false
                        o.wipeSetup()
                        setups.remove(j)
                    j -= 1
            s.tags := tg0
            s.tip  := tp0
            s.drawTsLabel(st.name, tsT, ext, tg0, tp0)
            s.drawOB(t0)
            s.drawTrade(t0)
        else
            keep := false
    else
        s.tags := tags
        s.tip  := tip
        s.drawTsLabel(st.name, tsT, ext, tags, tip)
        s.drawC2(st.name, c2Mid, c2T, c2C)
        bool lateCsd = entryModel == EM_CSD and not liveEntry and (dir == 1 ? r.lCsdT : r.hCsdT) == st.prevHtfT
        if not showTrade
            s.state := 7
        else if entryModel == EM_TS or lateCsd
            // enter at the close of the higher-timeframe Turtle Soup candle
            s.entry := px
            s.t1ok  := dir == -1 ? px > s.tp1 : px < s.tp1
            if rrOk
                s.state := 1
                s.drawOB(t0)
                s.drawTrade(t0)
            else
                s.state := 7
        else if entryModel == EM_CSD
            // with the default entry the signal comes from the entry timeframe; nothing to wait for here
            s.state := 7
        else
            s.state := 0
    if keep
        setups.push(s)
        // keep only the newest 'keepSetups' of this timeframe (a trade that is still open always stays)
        int cnt = 0
        int i = setups.size() - 1
        while i >= 0
            Setup o = setups.get(i)
            if o.tfName == st.name
                cnt += 1
                if cnt > keepSetups and o.state >= 3
                    o.wipeSetup()
                    setups.remove(i)
            i -= 1
    keep

// The higher-timeframe candle closed without confirming the Turtle Soup of an early signal
f_expireLive(string tfName) =>
    if setups.size() > 0
        for i = setups.size() - 1 to 0
            Setup s = setups.get(i)
            if s.tfName == tfName and s.live
                s.live := false
                s.note := "(HTF candle did not confirm the Ts)"
                s.refreshEntry()
    true

// A Turtle Soup of range r has been confirmed by the close of the higher-timeframe candle
// (first sweep, or a repeat sweep of a CRT that is already in play).
method tsConfirm(TFState st, Rng r, int dir, float h1, float l1, float c1, int t1, int tc1, int tsT, int srcId, float pH, float pL, TFState par) =>
    if dir == -1
        r.tapsH += 1
    else
        r.tapsL += 1
    r.lastDir := dir
    int   taps  = dir == -1 ? r.tapsH : r.tapsL
    float ext   = dir == -1 ? h1 : l1
    float bnd   = dir == -1 ? r.hi : r.lo
    bool  timed = f_isTimed(st.name, r.c1C, t1, tc1)
    bool  parS  = not na(pH) and not na(pL) and (dir == -1 ? h1 > pH : l1 < pL)
    bool  ctr   = f_counterHTF(st.rank, dir)
    [klP, klT]  = f_klFind(par, dir, ext, bnd, t1)
    bool  atKL  = not na(klP) or f_atKL(dir, ext, bnd)
    string tags = ""
    if taps > 1
        tags += " x" + str.tostring(taps)
    if timeMode != TM_OFF and timed
        tags += " T"
    if tagParent and parS
        tags += " HTF"
    if atKL
        tags += " KL"
    if tagCounter and ctr
        tags += " !"
    string tip = st.name + (dir == -1 ? " bearish" : " bullish") + " Turtle Soup" + (taps > 1 ? " (sweep number " + str.tostring(taps) + " of this side of the CRT)" : "") + "\nCRT High " + f_px(r.hi) + "\nCRT Low " + f_px(r.lo) + "\n50% " + f_px((r.hi + r.lo) / 2) + "\nInside bars before the first sweep: " + str.tostring(r.insideN) + (timed ? "\nT = formed at a key time" : "") + (parS ? "\nHTF = also swept the parent timeframe's range" : "") + (atKL ? "\nKL = the sweep took a key level" + (na(klP) ? "" : " (" + st.parName + " swing at " + f_px(klP) + ")") : "") + (ctr ? "\n! = a higher-timeframe trade in the opposite direction has not reached its target" : "")
    // the key level that was taken: a line from the parent-timeframe swing to the sweep
    if showKLauto and showTS and not na(klP) and na(r.klLn)
        r.klLn := f_ln(klT, klP, tsT, klP, klCol, klStyle, klWidth)
        r.klLb := label.new(klT, klP, "KL " + st.parName, xloc = xloc.bar_time, color = NOBG, style = dir == -1 ? label.style_label_lower_right : label.style_label_upper_right, textcolor = klCol, size = lblSize)
    // 50% of candle 2 (drawn once per candle, even when the candle swept two ranges)
    float c2Mid = st.c2Seen == t1 ? na : (h1 + l1) / 2
    st.c2Seen := t1
    // signals taken on the entry timeframe during this candle are now confirmed; if there was none,
    // the Ts label is drawn here
    bool conf = false
    if setups.size() > 0
        for i = setups.size() - 1 to 0
            Setup s = setups.get(i)
            if s.tfName == st.name and s.live and s.dir == dir and (s.rid == r.id or s.rid == srcId)
                s.live := false
                s.note := "(Ts confirmed)"
                s.rg   := r
                s.rid  := r.id
                label.set_text(s.lbTS, f_tsText(st.name, dir, tags))
                label.set_tooltip(s.lbTS, tip)
                if not conf
                    s.drawC2(st.name, c2Mid, t1, tc1)
                s.refreshEntry()
                conf := true
    // one Ts label per candle and direction, on the newest range that was swept
    bool again = dir == 1 ? st.tsUpSeen == t1 : st.tsDnSeen == t1
    if dir == 1
        st.tsUpSeen := t1
    else
        st.tsDnSeen := t1
    if not conf and not again
        f_newSetup(st, r, dir, ext, c1, tsT, time, tags, tip, false, c2Mid, t1, tc1)
    true

// A waiting range (or the previous candle) becomes a CRT in play. The sweep that is being followed on the
// entry timeframe is carried over.
f_promote(TFState st, Rng src) =>
    Rng r = f_rng(st.rank, 2, src.hi, src.lo, src.c1T, src.c1C)
    r.insideN  := src.insideN
    r.hOut     := src.hOut
    r.lOut     := src.lOut
    r.hCsdT    := src.hCsdT
    r.lCsdT    := src.lCsdT
    r.hExt     := src.hExt
    r.hExtT    := src.hExtT
    r.hOb      := src.hOb
    r.hObThick := src.hObThick
    r.hObO     := src.hObO
    r.hObH     := src.hObH
    r.hObL     := src.hObL
    r.hObT     := src.hObT
    r.lExt     := src.lExt
    r.lExtT    := src.lExtT
    r.lOb      := src.lOb
    r.lObThick := src.lObThick
    r.lObO     := src.lObO
    r.lObH     := src.lObH
    r.lObL     := src.lObL
    r.lObT     := src.lObT
    r

// May a range that is not yet a CRT in play give a signal? (the "timed setups only" filter of the settings)
f_passTime(TFState st, Rng r) =>
    timeMode != TM_FIL or st.name == "1M" or f_isTimed(st.name, r.c1C, st.htfT, st.htfT + (r.c1C - r.c1T))

// ═════════════════════════════════════════════════════════════════════════════
//  ENTRY TIMEFRAME : SWEEP, ORDER BLOCK, SIGNAL  (the trader's rule)
// ═════════════════════════════════════════════════════════════════════════════
// Prints one signal. A waiting range (or the previous candle) becomes a CRT in play at its first signal.
method giveSignal(TFState st, Rng r, int dir, float ec, int et) =>
    string m = ""
    bool  fresh = r.kind != 2
    int   lastF = dir == 1 ? st.freshUpT : st.freshDnT
    bool  okNew = not fresh or (not r.used and (na(lastF) or lastF != st.htfT) and f_passTime(st, r))
    float ext   = dir == 1 ? r.lExt : r.hExt
    int   extT  = dir == 1 ? r.lExtT : r.hExtT
    bool  first = na(st.sigT) or st.sigT != et
    if first and okNew and showTrade and entryModel == EM_CSD and liveEntry and not f_openSetup(r.id, dir)
        Rng tgt = r
        if fresh
            tgt := f_promote(st, r)
        if f_newSetup(st, tgt, dir, ext, ec, extT, sigBarT, "", "", true, na, na, na)
            st.sigT := et
            tgt.lastDir := dir
            if fresh
                r.used := true
                r.hOut := false
                r.lOut := false
                if dir == 1
                    st.freshUpT := st.htfT
                else
                    st.freshDnT := st.htfT
                st.plays.push(tgt)
                tgt.drawPlay(st)
                while st.plays.size() > maxPlays
                    st.retire(0)
                st.recolor()
            m := syminfo.ticker + " " + st.name + (dir == 1 ? " CRT BUY: price swept " + f_px(r.lo) : " CRT SELL: price swept " + f_px(r.hi)) + " and closed back inside the range. Entry " + f_px(ec) + ", SL " + f_px(ext) + ", 50% " + f_px((r.hi + r.lo) / 2) + ", 100% " + f_px(dir == 1 ? r.hi : r.lo) + "\n"
    m

// One range, one closed entry-timeframe candle.
//   BUY side : price trades below the CRT Low (the sweep). The Order Block is the last thick bearish candle
//              that pierces the CRT Low; if no thick candle pierces it, the candle that first pierced it.
//              As soon as a candle closes back inside the range (above the CRT Low; or above the Order Block
//              candle, when that is chosen in the settings) -> BUY.
//              Stop loss = the lowest low of the sweep.
//   SELL side: the mirror image at the CRT High.
// A single candle that swept both ends of the range gives no signal.
method entryOn(TFState st, Rng r, bool allowBear, bool allowBull, bool thick, float eo, float eh, float el, float ec, int et, int etHi, int etLo) =>
    string m = ""
    bool buyNow  = false
    bool sellNow = false
    // ── sweep of the CRT Low
    if el < r.lo
        if not r.lOut
            r.lOut  := true
            r.lExt  := el
            r.lExtT := etLo
            r.lOb   := false
        else if el < r.lExt
            r.lExt  := el
            r.lExtT := etLo
        bool strong = thick and ec < eo
        if eh >= r.lo and (strong or not r.lOb)
            r.lOb      := true
            r.lObThick := strong
            r.lObO     := eo
            r.lObH     := eh
            r.lObL     := el
            r.lObT     := et
    if r.lOut
        float lvl = r.lo
        if not csdRng and r.lOb and r.lObThick
            lvl := math.max(r.lo, csdWick ? r.lObH : r.lObO)
        if ec >= r.hi
            r.lOut := false
        else if ec > lvl
            // a candle has closed back inside the range (and above the Order Block when that is asked for)
            r.lOut  := false
            r.lCsdT := st.htfT
            buyNow  := true
    // ── sweep of the CRT High
    if eh > r.hi
        if not r.hOut
            r.hOut  := true
            r.hExt  := eh
            r.hExtT := etHi
            r.hOb   := false
        else if eh > r.hExt
            r.hExt  := eh
            r.hExtT := etHi
        bool strong = thick and ec > eo
        if el <= r.hi and (strong or not r.hOb)
            r.hOb      := true
            r.hObThick := strong
            r.hObO     := eo
            r.hObH     := eh
            r.hObL     := el
            r.hObT     := et
    if r.hOut
        float lvl = r.hi
        if not csdRng and r.hOb and r.hObThick
            lvl := math.min(r.hi, csdWick ? r.hObL : r.hObO)
        if ec <= r.lo
            r.hOut := false
        else if ec < lvl
            // a candle has closed back inside the range (and below the Order Block when that is asked for)
            r.hOut  := false
            r.hCsdT := st.htfT
            sellNow := true
    if buyNow and not sellNow and allowBull
        m := st.giveSignal(r, 1, ec, et)
    else if sellNow and not buyNow and allowBear
        m := st.giveSignal(r, -1, ec, et)
    m

// Runs on every chart bar for one timeframe, BEFORE process(), with the last closed entry-timeframe candle.
// Ranges are checked from the newest CRT in play to the oldest, then the waiting range, then the previous
// candle. While a newer range is being swept, the same move is not read again on an older range.
// Returns an alert text when a signal was given.
method entryStep(TFState st, bool ok, bool show, bool eNew, float eo, float eh, float el, float ec, int et) =>
    string m = ""
    st.ensure()
    st.eNew := false
    // chart bars that made the high and the low of the entry-timeframe candle that has just closed
    int xHiT = st.eHiT
    int xLoT = st.eLoT
    if eNew or na(st.eHi)
        st.eHi  := high
        st.eLo  := low
        st.eHiT := time
        st.eLoT := time
    else
        if high > st.eHi
            st.eHi  := high
            st.eHiT := time
        if low < st.eLo
            st.eLo  := low
            st.eLoT := time
    if ok and eNew and not na(eo) and not na(eh) and not na(el) and not na(ec)
        float body  = math.abs(ec - eo)
        float avg   = st.bodies.size() > 0 ? st.bodies.avg() : na
        bool  thick = obThick <= 0 or (not na(avg) and body >= obThick * avg)
        st.eNew   := true
        st.eo     := eo
        st.eh     := eh
        st.el     := el
        st.ec     := ec
        st.et     := et
        st.eThick := thick
        int etHi = na(xHiT) ? et : xHiT
        int etLo = na(xLoT) ? et : xLoT
        if show and st.hasM
            bool bearBusy = false
            bool bullBusy = false
            int i = st.plays.size() - 1
            while i >= 0
                Rng r = st.plays.get(i)
                bool wasH = r.hOut or eh > r.hi
                bool wasL = r.lOut or el < r.lo
                m += st.entryOn(r, true, true, thick, eo, eh, el, ec, et, etHi, etLo)
                if wasH
                    bearBusy := true
                if wasL
                    bullBusy := true
                i -= 1
            // the waiting range and the previous candle are read before any of them becomes a CRT in play
            Rng  mo   = st.m
            Rng  po   = st.p
            bool hadP = st.hasP
            bool mH   = mo.hOut or eh > mo.hi
            bool mL   = mo.lOut or el < mo.lo
            m += st.entryOn(mo, not bearBusy, not bullBusy, thick, eo, eh, el, ec, et, etHi, etLo)
            if hadP
                m += st.entryOn(po, not bearBusy and not mH, not bullBusy and not mL, thick, eo, eh, el, ec, et, etHi, etLo)
        st.bodies.push(body)
        if st.bodies.size() > obThickLen
            st.bodies.shift()
    m

// ═════════════════════════════════════════════════════════════════════════════
//  HIGHER-TIMEFRAME CRT ENGINE
// ═════════════════════════════════════════════════════════════════════════════
// High / Low of the waiting range that was in force for the candle that has just closed
method refHi(TFState st) =>
    st.updBar == bar_index ? st.oldH : st.crh

method refLo(TFState st) =>
    st.updBar == bar_index ? st.oldL : st.crl

// Runs on every chart bar for one timeframe.
// Returns +1 when a bullish Turtle Soup was confirmed on this bar, -1 for a bearish one, 0 otherwise.
//
// RULE 1  A candle becomes the CRT range when a later candle wicks beyond its high or low and closes back
//         inside. The later candle can be the very next one, or come after inside bars. The range candle may
//         itself sit inside an older, larger candle.
// RULE 2  The CRT range then stays in play, with both its High and its Low, until a candle of its timeframe
//         closes outside it. Each later wick beyond one end that closes back inside is another Turtle Soup
//         of the same range (x2, x3 ...), with the opposite end as the 100% target.
method process(TFState st, bool ok, bool show, float h1, float l1, float c1, int t1, int tc1, int tn, bool isNew, float pH, float pL, TFState par) =>
    int sig = 0
    st.swings(ok, h1, l1, t1, isNew)
    if ok
        // time of the bar that made the high / low of the HTF candle that is forming
        int prvHiT = st.curHiT
        int prvLoT = st.curLoT
        if isNew or na(st.curHi)
            st.curHi  := high
            st.curLo  := low
            st.curHiT := time
            st.curLoT := time
        else
            if high > st.curHi
                st.curHi  := high
                st.curHiT := time
            if low < st.curLo
                st.curLo  := low
                st.curLoT := time
        if isNew or na(st.htfT)
            st.prevHtfT := st.htfT
            st.htfT     := tn
        // a higher-timeframe candle has just closed: h1 / l1 / c1 are its final values
        if isNew and not na(h1) and not na(l1) and not na(c1)
            int tsHiT = na(prvHiT) ? t1 : prvHiT
            int tsLoT = na(prvLoT) ? t1 : prvLoT
            if show and setups.size() > 0
                for i = 0 to setups.size() - 1
                    Setup o = setups.get(i)
                    if o.tfName == st.name
                        o.age += 1
            // ── a) CRT ranges in play: closed outside (ended) or swept again on one side
            bool bearSw = false
            bool bullSw = false
            if st.plays.size() > 0
                int i = st.plays.size() - 1
                while i >= 0
                    Rng r = st.plays.get(i)
                    bool wHi = h1 > r.hi
                    bool wLo = l1 < r.lo
                    if c1 > r.hi or c1 < r.lo
                        r.status := 2
                    else if wHi and not wLo
                        bearSw := true
                        if show
                            st.tsConfirm(r, -1, h1, l1, c1, t1, tc1, tsHiT, r.id, pH, pL, par)
                            sig := -1
                    else if wLo and not wHi
                        bullSw := true
                        if show
                            st.tsConfirm(r, 1, h1, l1, c1, t1, tc1, tsLoT, r.id, pH, pL, par)
                            sig := 1
                    if r.status != 0
                        st.retire(i)
                    i -= 1
            if not st.hasM
                st.m    := f_rng(st.rank, 0, h1, l1, t1, tc1)
                st.hasM := true
            else
                // ── b) a new CRT: first against the waiting range, then against the previous candle
                Rng  mo  = st.m
                bool up  = h1 > mo.hi
                bool dn  = l1 < mo.lo
                int  dir = 0
                Rng  src = mo
                if up and not dn and c1 < mo.hi
                    dir := -1
                    st.lastEvt := "-Ts: wick above, close inside = bearish"
                else if dn and not up and c1 > mo.lo
                    dir := 1
                    st.lastEvt := "+Ts: wick below, close inside = bullish"
                else if up and dn
                    st.lastEvt := "Both sides taken (no signal)"
                else if up
                    st.lastEvt := "Close above = higher prices"
                else if dn
                    st.lastEvt := "Close below = lower prices"
                else
                    st.lastEvt := "Inside bar"
                    if st.hasP
                        Rng  pr  = st.p
                        bool up2 = h1 > pr.hi
                        bool dn2 = l1 < pr.lo
                        if up2 and not dn2 and c1 < pr.hi
                            dir := -1
                            src := pr
                            st.lastEvt := "-Ts of the previous candle = bearish"
                        else if dn2 and not up2 and c1 > pr.lo
                            dir := 1
                            src := pr
                            st.lastEvt := "+Ts of the previous candle = bullish"
                // a repeat sweep of a CRT that is already in play is not a new range
                if (dir == -1 and bearSw) or (dir == 1 and bullSw)
                    dir := 0
                if dir != 0
                    bool timed = f_isTimed(st.name, src.c1C, t1, tc1)
                    bool pass  = timeMode != TM_FIL or timed or st.name == "1M"
                    if show and pass
                        Rng r = f_promote(st, src)
                        st.plays.push(r)
                        r.drawPlay(st)
                        st.tsConfirm(r, dir, h1, l1, c1, t1, tc1, dir == -1 ? tsHiT : tsLoT, src.id, pH, pL, par)
                        sig := dir
                        while st.plays.size() > maxPlays
                            st.retire(0)
                // early signals whose Turtle Soup was not confirmed by this close
                f_expireLive(st.name)
                // ── c) the waiting range and the previous candle for the next candle
                if up or dn
                    st.oldH   := mo.hi
                    st.oldL   := mo.lo
                    st.updBar := bar_index
                    st.m      := f_rng(st.rank, 0, h1, l1, t1, tc1)
                    st.hasP   := false
                else
                    mo.insideN += 1
                    st.p    := f_rng(st.rank, 1, h1, l1, t1, tc1)
                    st.hasP := true
            st.crh := st.m.hi
            st.crl := st.m.lo
            st.recolor()
        // ── drawings that follow the last bar
        if barstate.islast
            color noBg = NOBG
            // CRTs in play
            if st.plays.size() > 0
                for i = 0 to st.plays.size() - 1
                    Rng r = st.plays.get(i)
                    line.set_x2(r.lnH, tRight)
                    line.set_x2(r.lnL, tRight)
                    line.set_x2(r.lnEq, tRight)
                    label.set_x(r.lbH, tRight)
                    label.set_x(r.lbL, tRight)
            // the waiting range (last candle that broke the range before it)
            if show and showTS and showActive and st.hasM
                if na(st.actH)
                    st.actH  := f_ln(st.m.c1T, st.crh, tRight, st.crh, st.col, actStyle, actWidth)
                    st.actL  := f_ln(st.m.c1T, st.crl, tRight, st.crl, st.col, actStyle, actWidth)
                    st.actHL := f_txt(tRight, st.crh, st.name + " range high", st.col, size.tiny, "")
                    st.actLL := f_txt(tRight, st.crl, st.name + " range low", st.col, size.tiny, "")
                else
                    line.set_xy1(st.actH, st.m.c1T, st.crh)
                    line.set_xy2(st.actH, tRight, st.crh)
                    line.set_xy1(st.actL, st.m.c1T, st.crl)
                    line.set_xy2(st.actL, tRight, st.crl)
                    label.set_xy(st.actHL, tRight, st.crh)
                    label.set_xy(st.actLL, tRight, st.crl)
            // key levels for this CRT timeframe: the nearest untaken swing high above and swing low below,
            // taken from the timeframe one step up (divide-by-4 rule)
            if show and showKLauto and showKLcrt
                [kHp, kHt] = f_klNext(par, 1, close)
                [kLp, kLt] = f_klNext(par, -1, close)
                if na(kHp)
                    line.delete(st.klHiLn)
                    label.delete(st.klHiLb)
                    st.klHiLn := na
                    st.klHiLb := na
                else if na(st.klHiLn)
                    st.klHiLn := f_ln(kHt, kHp, tRight, kHp, klCol, klStyle, klWidth)
                    st.klHiLb := f_txt(tRight, kHp, "KL " + st.parName + " swing high (for " + st.name + " CRT)", klCol, lblSize, "")
                else
                    line.set_xy1(st.klHiLn, kHt, kHp)
                    line.set_xy2(st.klHiLn, tRight, kHp)
                    label.set_xy(st.klHiLb, tRight, kHp)
                if na(kLp)
                    line.delete(st.klLoLn)
                    label.delete(st.klLoLb)
                    st.klLoLn := na
                    st.klLoLb := na
                else if na(st.klLoLn)
                    st.klLoLn := f_ln(kLt, kLp, tRight, kLp, klCol, klStyle, klWidth)
                    st.klLoLb := f_txt(tRight, kLp, "KL " + st.parName + " swing low (for " + st.name + " CRT)", klCol, lblSize, "")
                else
                    line.set_xy1(st.klLoLn, kLt, kLp)
                    line.set_xy2(st.klLoLn, tRight, kLp)
                    label.set_xy(st.klLoLb, tRight, kLp)
    sig

// ═════════════════════════════════════════════════════════════════════════════
//  HIGHER-TIMEFRAME DATA  (completed candles only, no repainting)
// ═════════════════════════════════════════════════════════════════════════════
int  chartSec = timeframe.in_seconds()
bool ok1H = timeframe.in_seconds("60") >= chartSec
bool ok4H = timeframe.in_seconds("240") >= chartSec
bool okD  = timeframe.in_seconds("D") >= chartSec
bool okW  = timeframe.in_seconds("W") >= chartSec
bool okM  = timeframe.in_seconds("M") >= chartSec
bool okY  = timeframe.in_seconds("12M") >= chartSec

// A timeframe below the chart timeframe is replaced by the chart timeframe and then ignored (ok.. = false)
string tf1H = ok1H ? "60" : timeframe.period
string tf4H = ok4H ? "240" : timeframe.period
string tfD  = okD ? "D" : timeframe.period
string tfW  = okW ? "W" : timeframe.period
string tfM  = okM ? "M" : timeframe.period
string tfY  = okY ? "12M" : timeframe.period

// [1] + lookahead_on = values of the last CLOSED higher-timeframe candle, available at the open of the next one.
// The last two values are the opening and closing time of the candle that is forming.
[h1H, l1H, c1H, t1H, tc1H, tn1H, tcn1H] = request.security(syminfo.tickerid, tf1H, [high[1], low[1], close[1], time[1], time_close[1], time, time_close], lookahead = barmerge.lookahead_on)
[h4H, l4H, c4H, t4H, tc4H, tn4H, tcn4H] = request.security(syminfo.tickerid, tf4H, [high[1], low[1], close[1], time[1], time_close[1], time, time_close], lookahead = barmerge.lookahead_on)
[hD, lD, cD, tD, tcD, tnD, tcnD] = request.security(syminfo.tickerid, tfD, [high[1], low[1], close[1], time[1], time_close[1], time, time_close], lookahead = barmerge.lookahead_on)
[hW, lW, cW, tW, tcW, tnW, tcnW] = request.security(syminfo.tickerid, tfW, [high[1], low[1], close[1], time[1], time_close[1], time, time_close], lookahead = barmerge.lookahead_on)
[hM, lM, cM, tM, tcM, tnM, tcnM] = request.security(syminfo.tickerid, tfM, [high[1], low[1], close[1], time[1], time_close[1], time, time_close], lookahead = barmerge.lookahead_on)
// The yearly candle is only used for key levels
[hY, lY, cY, tY, tcY, tnY] = request.security(syminfo.tickerid, tfY, [high[1], low[1], close[1], time[1], time_close[1], time], lookahead = barmerge.lookahead_on)

bool new1H = tn1H != tn1H[1]
bool new4H = tn4H != tn4H[1]
bool newD  = tnD != tnD[1]
bool newW  = tnW != tnW[1]
bool newM  = tnM != tnM[1]
bool newY  = tnY != tnY[1]

// Entry timeframes (last CLOSED candle). An entry timeframe must be lower than its CRT timeframe and not
// lower than the chart timeframe; otherwise the chart timeframe is used for that CRT.
bool eOk1H = timeframe.in_seconds(eTf1HIn) >= chartSec and timeframe.in_seconds(eTf1HIn) < timeframe.in_seconds("60")
bool eOk4H = timeframe.in_seconds(eTf4HIn) >= chartSec and timeframe.in_seconds(eTf4HIn) < timeframe.in_seconds("240")
bool eOkD  = timeframe.in_seconds(eTfDIn) >= chartSec and timeframe.in_seconds(eTfDIn) < timeframe.in_seconds("D")
bool eOkW  = timeframe.in_seconds(eTfWIn) >= chartSec and timeframe.in_seconds(eTfWIn) < timeframe.in_seconds("W")
bool eOkM  = timeframe.in_seconds(eTfMIn) >= chartSec and timeframe.in_seconds(eTfMIn) < timeframe.in_seconds("M")
string eTf1H = eOk1H ? eTf1HIn : timeframe.period
string eTf4H = eOk4H ? eTf4HIn : timeframe.period
string eTfD  = eOkD ? eTfDIn : timeframe.period
string eTfW  = eOkW ? eTfWIn : timeframe.period
string eTfM  = eOkM ? eTfMIn : timeframe.period

[eo1H, eh1H, el1H, ec1H, et1H, etn1H] = request.security(syminfo.tickerid, eTf1H, [open[1], high[1], low[1], close[1], time[1], time], lookahead = barmerge.lookahead_on)
[eo4H, eh4H, el4H, ec4H, et4H, etn4H] = request.security(syminfo.tickerid, eTf4H, [open[1], high[1], low[1], close[1], time[1], time], lookahead = barmerge.lookahead_on)
[eoD, ehD, elD, ecD, etD, etnD] = request.security(syminfo.tickerid, eTfD, [open[1], high[1], low[1], close[1], time[1], time], lookahead = barmerge.lookahead_on)
[eoW, ehW, elW, ecW, etW, etnW] = request.security(syminfo.tickerid, eTfW, [open[1], high[1], low[1], close[1], time[1], time], lookahead = barmerge.lookahead_on)
[eoM, ehM, elM, ecM, etM, etnM] = request.security(syminfo.tickerid, eTfM, [open[1], high[1], low[1], close[1], time[1], time], lookahead = barmerge.lookahead_on)

bool eNew1H = etn1H != etn1H[1]
bool eNew4H = etn4H != etn4H[1]
bool eNewD  = etnD != etnD[1]
bool eNewW  = etnW != etnW[1]
bool eNewM  = etnM != etnM[1]

// Each timeframe and the timeframe its key levels come from (divide-by-4 rule of the notes)
var TFState st1H = TFState.new("1H", 1, col1H, "4H")
var TFState st4H = TFState.new("4H", 2, col4H, "1D")
var TFState stD  = TFState.new("1D", 3, colD, "1W")
var TFState stW  = TFState.new("1W", 4, colW, "1M")
var TFState stM  = TFState.new("1M", 5, colM, "1Y")
var TFState stY  = TFState.new("1Y", 6, colM, "")
// colour of the CRTs that are no longer the newest one of their timeframe
st1H.pcol := usePrevCol ? prev1H : col1H
st4H.pcol := usePrevCol ? prev4H : col4H
stD.pcol  := usePrevCol ? prevD : colD
stW.pcol  := usePrevCol ? prevW : colW
stM.pcol  := usePrevCol ? prevM : colM

// The timeframes in two lists, for the loops further down: highest first, and lowest first with its parent
var array<TFState> tfDown = array.new<TFState>()
var array<TFState> tfUp   = array.new<TFState>()
var array<TFState> tfPar  = array.new<TFState>()
if barstate.isfirst
    tfDown.push(stM)
    tfDown.push(stW)
    tfDown.push(stD)
    tfDown.push(st4H)
    tfDown.push(st1H)
    tfUp.push(st1H)
    tfUp.push(st4H)
    tfUp.push(stD)
    tfUp.push(stW)
    tfUp.push(stM)
    tfPar.push(st4H)
    tfPar.push(stD)
    tfPar.push(stW)
    tfPar.push(stM)
    tfPar.push(stY)

// Yearly swings first. Then, for each timeframe: the entry-timeframe step (it belongs to the candle that is
// still forming) followed by the higher-timeframe step. Highest timeframe first, so each lower one can read
// its parent's swings and range.
stY.swings(okY, hY, lY, tY, newY)
string liveM  = stM.entryStep(okM, inM, eNewM, eoM, ehM, elM, ecM, etM)
int    sigM   = stM.process(okM, inM, hM, lM, cM, tM, tcM, tnM, newM, na, na, stY)
string liveW  = stW.entryStep(okW, inW, eNewW, eoW, ehW, elW, ecW, etW)
int    sigW   = stW.process(okW, inW, hW, lW, cW, tW, tcW, tnW, newW, stM.refHi(), stM.refLo(), stM)
string liveD  = stD.entryStep(okD, inD, eNewD, eoD, ehD, elD, ecD, etD)
int    sigD   = stD.process(okD, inD, hD, lD, cD, tD, tcD, tnD, newD, stW.refHi(), stW.refLo(), stW)
string live4H = st4H.entryStep(ok4H, in4H, eNew4H, eo4H, eh4H, el4H, ec4H, et4H)
int    sig4H  = st4H.process(ok4H, in4H, h4H, l4H, c4H, t4H, tc4H, tn4H, new4H, stD.refHi(), stD.refLo(), stD)
string live1H = st1H.entryStep(ok1H, in1H, eNew1H, eo1H, eh1H, el1H, ec1H, et1H)
int    sig1H  = st1H.process(ok1H, in1H, h1H, l1H, c1H, t1H, tc1H, tn1H, new1H, st4H.refHi(), st4H.refLo(), st4H)

// ═════════════════════════════════════════════════════════════════════════════
//  TRADE MANAGEMENT  (entry, 50% target, 100% target, stop loss)
// ═════════════════════════════════════════════════════════════════════════════
string tradeMsg = liveM + liveW + liveD + live4H + live1H
if setups.size() > 0
    for i = setups.size() - 1 to 0
        Setup s = setups.get(i)
        int ev = s.update()
        if ev == 2
            s.t1hit := true
        if ev > 0
            string side = s.dir == -1 ? "SELL" : "BUY"
            string what = switch ev
                1 => "entry triggered at " + f_px(s.entry) + ", SL " + f_px(s.sl) + ", 50% " + f_px(s.tp1) + ", 100% " + f_px(s.tp2)
                2 => "50% of the CRT range reached at " + f_px(s.tp1) + ", close half"
                3 => "100% of the CRT range reached at " + f_px(s.tp2) + ", close the rest"
                4 => "stop loss hit at " + f_px(s.sl)
                5 => "stopped at breakeven after the 50% target"
                => "waiting entry cancelled"
            tradeMsg += syminfo.ticker + " " + s.tfName + " CRT " + side + ": " + what + "\n"

if useAlerts and tradeMsg != ""
    alert(tradeMsg, alert.freq_once_per_bar)

// Turtle Soup alerts (one combined message per bar)
f_tsMsg(TFState st, int sig) =>
    string m = ""
    if sig != 0 and setups.size() > 0
        for i = setups.size() - 1 to 0
            Setup s = setups.get(i)
            if s.tfName == st.name and s.dir == sig
                m := syminfo.ticker + " " + st.name + (sig == -1 ? " bearish Turtle Soup (-Ts) confirmed by the candle close: wick above CRT High, close back inside. Target CRT Low " : " bullish Turtle Soup (+Ts) confirmed by the candle close: wick below CRT Low, close back inside. Target CRT High ") + f_px(s.tp2) + ", 50% level " + f_px(s.tp1) + "\n"
                break
    m

string tsMsg = f_tsMsg(stM, sigM) + f_tsMsg(stW, sigW) + f_tsMsg(stD, sigD) + f_tsMsg(st4H, sig4H) + f_tsMsg(st1H, sig1H)
if useAlerts and tsMsg != ""
    alert(tsMsg, alert.freq_once_per_bar)

bool anyBull = sigM == 1 or sigW == 1 or sigD == 1 or sig4H == 1 or sig1H == 1
bool anyBear = sigM == -1 or sigW == -1 or sigD == -1 or sig4H == -1 or sig1H == -1
alertcondition(anyBull, "Bullish Turtle Soup (+Ts) on any enabled timeframe", "Bullish CRT Turtle Soup (+Ts) confirmed")
alertcondition(anyBear, "Bearish Turtle Soup (-Ts) on any enabled timeframe", "Bearish CRT Turtle Soup (-Ts) confirmed")

// ═════════════════════════════════════════════════════════════════════════════
//  DASHBOARD
// ═════════════════════════════════════════════════════════════════════════════
f_tradeText(string tfName) =>
    string res = "-"
    if setups.size() > 0
        for i = setups.size() - 1 to 0
            Setup s = setups.get(i)
            if s.tfName == tfName and s.state != 7
                res := (s.dir == -1 ? "SELL: " : "BUY: ") + s.statusText()
                break
    res

f_tradeShort(string tfName) =>
    string res = "-"
    if setups.size() > 0
        for i = setups.size() - 1 to 0
            Setup s = setups.get(i)
            if s.tfName == tfName and s.state != 7
                res := (s.dir == -1 ? "SELL " : "BUY ") + s.statusShort()
                break
    res

// The newest CRT range in play of a timeframe
f_lastTs(Rng r) =>
    r.lastDir == -1 ? "-Ts" + (r.tapsH > 1 ? " x" + str.tostring(r.tapsH) : "") : r.lastDir == 1 ? "+Ts" + (r.tapsL > 1 ? " x" + str.tostring(r.tapsL) : "") : ""

f_playText(TFState st) =>
    string res = "none"
    if st.inited
        if st.plays.size() > 0
            Rng r = st.plays.last()
            res := f_px(r.hi) + " / " + f_px(r.lo) + "\nlast sweep: " + f_lastTs(r) + (st.plays.size() > 1 ? "\n+" + str.tostring(st.plays.size() - 1) + " older range(s) in play" : "")
    res

f_playShort(TFState st) =>
    string res = "-"
    if st.inited
        if st.plays.size() > 0
            Rng r = st.plays.last()
            res := f_px(r.hi) + " / " + f_px(r.lo) + "  " + f_lastTs(r)
    res

f_row(table t, int r, TFState st, bool ok, bool on, string eTxt) =>
    color txt = color.new(color.white, 0)
    string rngTxt  = na(st.crh) ? "-" : f_px(st.crh) + " / " + f_px(st.crl)
    string liveTxt = "-"
    color  liveCol = txt
    if not ok
        liveTxt := "chart timeframe is higher"
        liveCol := color.gray
    else if not on
        liveTxt := "switched off"
        liveCol := color.gray
    else if not na(st.crh) and not na(st.curHi)
        if close > st.crh
            liveTxt := "Trading above the range"
            liveCol := tsBullCol
        else if close < st.crl
            liveTxt := "Trading below the range"
            liveCol := tsBearCol
        else if st.curHi > st.crh and st.curLo < st.crl
            liveTxt := "Both sides swept"
        else if st.curHi > st.crh
            liveTxt := "High swept, back inside: possible -Ts"
            liveCol := tsBearCol
        else if st.curLo < st.crl
            liveTxt := "Low swept, back inside: possible +Ts"
            liveCol := tsBullCol
        else
            liveTxt := "Inside the range"
    f_cell(t, 0, r, st.name + eTxt, st.col, size.small, "")
    f_cell(t, 1, r, ok and on ? f_playText(st) : "-", txt, size.small, "")
    f_cell(t, 2, r, rngTxt, txt, size.small, "")
    f_cell(t, 3, r, liveTxt, liveCol, size.small, "")
    f_cell(t, 4, r, ok and on ? st.lastEvt : "-", txt, size.small, "")
    f_cell(t, 5, r, ok and on ? f_tradeText(st.name) : "-", txt, size.small, "")
    true

f_rowC(table t, int r, TFState st, string eTxt) =>
    color txt = color.new(color.white, 0)
    f_cell(t, 0, r, st.name + eTxt, st.col, dashTxt, "")
    f_cell(t, 1, r, f_playShort(st), txt, dashTxt, "")
    f_cell(t, 2, r, f_tradeShort(st.name), txt, dashTxt, "")
    true

var table dash = table.new(f_pos(dashPosIn), 7, 7, bgcolor = color.new(#131722, 15), frame_color = color.new(color.gray, 40), frame_width = 1, border_color = color.new(color.gray, 70), border_width = 1)

if showDash and barstate.islast
    color hdr = color.new(color.silver, 0)
    table.clear(dash, 0, 0, 6, 6)
    // the five timeframes, highest first
    array<bool>   dOk = array.from(okM, okW, okD, ok4H, ok1H)
    array<bool>   dIn = array.from(inM, inW, inD, in4H, in1H)
    array<string> dE  = array.from(" > " + (eOkM ? eTfMIn : "chart"), " > " + (eOkW ? eTfWIn : "chart"), " > " + (eOkD ? eTfDIn : "chart"), " > " + (eOk4H ? eTf4HIn : "chart"), " > " + (eOk1H ? eTf1HIn : "chart"))
    if dashMode == DB_FULL
        f_cell(dash, 0, 0, "CRT > entry TF", hdr, size.small, "")
        f_cell(dash, 1, 0, "CRT in play (High / Low)", hdr, size.small, "")
        f_cell(dash, 2, 0, "Last range candle", hdr, size.small, "")
        f_cell(dash, 3, 0, "Candle now forming", hdr, size.small, "")
        f_cell(dash, 4, 0, "Last closed candle", hdr, size.small, "")
        f_cell(dash, 5, 0, "Latest trade", hdr, size.small, "")
        for i = 0 to 4
            f_row(dash, i + 1, tfDown.get(i), dOk.get(i), dIn.get(i), dE.get(i))
    else
        // only the timeframes that are switched on and available on this chart
        f_cell(dash, 0, 0, "CRT > entry", hdr, dashTxt, "CRT timeframe and the timeframe its BUY / SELL signals are read on (set in group 6).")
        f_cell(dash, 1, 0, "CRT in play (H / L)", hdr, dashTxt, "")
        f_cell(dash, 2, 0, "Trade", hdr, dashTxt, "")
        int row = 1
        for i = 0 to 4
            if dOk.get(i) and dIn.get(i)
                f_rowC(dash, row, tfDown.get(i), dE.get(i))
                row += 1

// ═════════════════════════════════════════════════════════════════════════════
//  KEY LEVELS FOR THE CHART TIMEFRAME
// ═════════════════════════════════════════════════════════════════════════════
// The untaken swing highs and lows of the higher timeframe that belongs to this chart:
//   5m chart or lower -> 1H swings       15m to 1H chart -> 4H swings      2H to 4H chart -> Daily swings
//   Daily chart -> Weekly swings         Weekly chart -> Monthly swings    Monthly chart -> Yearly swings
// The timeframe can also be chosen by hand in the settings.
TFState klSt = st1H
string klName = "1H"
if klTfIn == KT_AUTO
    if chartSec > 300
        klSt   := st4H
        klName := "4H"
    if chartSec > 3600
        klSt   := stD
        klName := "1D"
    if chartSec > 14400
        klSt   := stW
        klName := "1W"
    if chartSec > 86400
        klSt   := stM
        klName := "1M"
    if chartSec > 604800
        klSt   := stY
        klName := "1Y"
else if klTfIn == "4H"
    klSt   := st4H
    klName := "4H"
else if klTfIn == "1D"
    klSt   := stD
    klName := "1D"
else if klTfIn == "1W"
    klSt   := stW
    klName := "1W"
else if klTfIn == "1M"
    klSt   := stM
    klName := "1M"
else if klTfIn == "1Y"
    klSt   := stY
    klName := "1Y"

var array<line>  klLines = array.new<line>()
var array<label> klLabs  = array.new<label>()
if barstate.islast
    // redraw from scratch on every update of the last bar
    while klLines.size() > 0
        line.delete(klLines.pop())
    while klLabs.size() > 0
        label.delete(klLabs.pop())
    if showKLauto and showKLchart
        color noBg = NOBG
        float refUp = close
        float refDn = close
        for n = 1 to klCount
            [pU, tU] = f_klNext(klSt, 1, refUp)
            if not na(pU)
                klLines.push(f_ln(tU, pU, tRight, pU, klCol, klStyle, klWidth))
                klLabs.push(f_txt(tRight, pU, "KL " + klName, klCol, lblSize, "Key level: untaken " + klName + " swing high at " + f_px(pU)))
                refUp := pU
            [pD, tD2] = f_klNext(klSt, -1, refDn)
            if not na(pD)
                klLines.push(f_ln(tD2, pD, tRight, pD, klCol, klStyle, klWidth))
                klLabs.push(f_txt(tRight, pD, "KL " + klName, klCol, lblSize, "Key level: untaken " + klName + " swing low at " + f_px(pD)))
                refDn := pD

// ═════════════════════════════════════════════════════════════════════════════
//  KEY LEVELS (placed by the trader)
// ═════════════════════════════════════════════════════════════════════════════
var array<line>  klULines = array.new<line>()
var array<label> klULabs  = array.new<label>()
if showKL and barstate.islast
    if klULines.size() == 0
        array<float> klU = array.from(kl1, kl2, kl3)
        for i = 0 to 2
            float v = klU.get(i)
            if v > 0
                klULines.push(line.new(bar_index, v, bar_index + 1, v, extend = extend.both, color = klCol, style = klStyle, width = klWidth))
                klULabs.push(f_txt(tRight, v, "KL", klCol, lblSize, ""))
    else
        for i = 0 to klULabs.size() - 1
            label.set_x(klULabs.get(i), tRight)

// ═════════════════════════════════════════════════════════════════════════════
//  HTF CANDLES  (drawn to the right of the last bar)
// ═════════════════════════════════════════════════════════════════════════════
// The candles are built from the chart's own bars, so they always agree with the chart.
// One higher-timeframe candle
type HC
    float o
    float h
    float l
    float c
    int   t
    int   tc
    int   bi

// The stored candles of one timeframe (the last one is the candle that is still forming)
type HSet
    string    name
    array<HC> cs

const int HC_KEEP = 40

var array<box>   hcBoxes = array.new<box>()
var array<line>  hcLines = array.new<line>()
var array<label> hcLabs  = array.new<label>()

// Adds the chart bar to the candles of one timeframe. Returns a text when the candle that has just closed
// completed a Fair Value Gap (used for the alert).
method feed(HSet hs, bool on, bool isNew, int tOpen, int tClose) =>
    string msg = ""
    if on
        if isNew or hs.cs.size() == 0
            hs.cs.push(HC.new(open, high, low, close, tOpen, tClose, bar_index))
            if hs.cs.size() > HC_KEEP
                hs.cs.shift()
            int n = hs.cs.size()
            if n >= 4
                HC a  = hs.cs.get(n - 4)
                HC c3 = hs.cs.get(n - 2)
                if c3.l > a.h
                    msg := syminfo.ticker + " " + hs.name + " bullish FVG " + f_px(a.h) + " - " + f_px(c3.l) + "\n"
                else if c3.h < a.l
                    msg := syminfo.ticker + " " + hs.name + " bearish FVG " + f_px(c3.h) + " - " + f_px(a.l) + "\n"
        else
            HC k = hs.cs.last()
            k.h := math.max(k.h, high)
            k.l := math.min(k.l, low)
            k.c := close
    msg

// Trend bias of a timeframe, from its closed candles (the rule of the notes):
//   close above the range high = higher prices (up)        close below the range low = lower prices (down)
//   wick above, close back inside = bearish (down)         wick below, close back inside = bullish (up)
// The range is the last candle that broke the range before it; inside bars leave the bias unchanged.
// Returns 1 (up), -1 (down) or 0 (not known yet).
f_bias(HSet hs) =>
    int b = 0
    int n = hs.cs.size()
    if n >= 3
        HC ref = hs.cs.get(0)
        for i = 1 to n - 2
            HC   k  = hs.cs.get(i)
            bool up = k.h > ref.h
            bool dn = k.l < ref.l
            if up and not dn
                b := k.c > ref.h ? 1 : -1
            else if dn and not up
                b := k.c < ref.l ? -1 : 1
            else if up and dn
                b := k.c > ref.h ? 1 : k.c < ref.l ? -1 : b
            if up or dn
                ref := k
    b

f_arrow(int b) =>
    b == 1 ? "↗" : b == -1 ? "↘" : "→"

// Text under a candle: its opening hour (1H, 4H), its day (1D), its start date (1W) or its month (1M),
// read on the clock chosen in group 7
f_hcTime(string tfName, int t, int tc) =>
    string s = ""
    if tfName == "1H" or tfName == "4H"
        int h   = hour(t, tzIn)
        int h12 = h % 12 == 0 ? 12 : h % 12
        s := str.tostring(h12) + (h < 12 ? "am" : "pm")
    else if tfName == "1D"
        int d = dayofweek(tc - 1, tzIn)
        s := d == dayofweek.monday ? "Mon" : d == dayofweek.tuesday ? "Tue" : d == dayofweek.wednesday ? "Wed" : d == dayofweek.thursday ? "Thu" : d == dayofweek.friday ? "Fri" : d == dayofweek.saturday ? "Sat" : "Sun"
    else if tfName == "1W"
        s := str.tostring(dayofmonth(t, tzIn)) + "/" + str.tostring(month(t, tzIn))
    else
        int mo = month(tc - 1, tzIn)
        s := mo == 1 ? "Jan" : mo == 2 ? "Feb" : mo == 3 ? "Mar" : mo == 4 ? "Apr" : mo == 5 ? "May" : mo == 6 ? "Jun" : mo == 7 ? "Jul" : mo == 8 ? "Aug" : mo == 9 ? "Sep" : mo == 10 ? "Oct" : mo == 11 ? "Nov" : "Dec"
    s

// Time left in the forming candle
f_hcLeft(int tc) =>
    int    ms = tc - timenow
    string s  = "n/a"
    if not na(ms) and ms > 0
        int sec = int(ms / 1000)
        int d   = int(sec / 86400)
        int h   = int((sec % 86400) / 3600)
        int mi  = int((sec % 3600) / 60)
        int ss  = sec % 60
        s := (d > 0 ? str.tostring(d) + "d " : "") + str.tostring(h, "00") + ":" + str.tostring(mi, "00") + ":" + str.tostring(ss, "00")
    s

// ─────────────────────────────────────────────────────────────────────────────
//  CRT SUB TYPES
// ─────────────────────────────────────────────────────────────────────────────
// Every closed higher-timeframe candle is a possible range candle (candle 1). What the following candles
// do with its high and low decides the sub type. The rules are written here for a sweep of the HIGH
// (a bearish CRT, delivery to the low); a sweep of the LOW is the mirror image.
//   ST1  candle 2 sweeps the high and closes back inside; candle 3 reaches the low.
//   ST2  the same, but the low is reached one candle later (candle 4).
//   ST3  candle 2 sweeps the high and reaches the low by itself.
//   ST4  one or more inside candles come first, then the sweep and the delivery.
//   ST5  candle 2 sweeps the high, an entry-timeframe candle closes back inside (the SELL), price does not
//        reach the low, and candle 2 closes above the high; a later candle then trades above candle 2's
//        high. A failed reversal that became a continuation.
//   ST6  two or more candles sweep the high, each closing back inside the range, before the delivery.
//   other          the low was reached, but the sequence fits none of the six
//   not completed  a single sweep that never reached the low (a candle closed beyond the swept end, or
//                  the formation ran out of candles)
// A formation is complete when price reaches the opposite end of the range. One formation per direction is
// followed on a timeframe at a time.
type Pat
    int   c1T
    float hi
    float lo
    int   stage   = 0
    int   dir     = 0
    int   nInside = 0
    int   nSweep  = 0
    int   nDeliv  = 0
    int   age     = 0
    int   swT
    int   endT
    int   extT
    float ext
    float c2Ext
    bool  r50     = false
    bool  r100    = false
    bool  kl      = false
    int   typ     = 0
    bool  done    = false
    bool  dead    = false
    bool  upSeen  = false
    bool  dnSeen  = false
    bool  backDn  = false
    bool  backUp  = false
    float minAfterUp
    float maxAfterDn

// Pat.stage : 0 waiting for the first sweep   1 swept, waiting for the delivery   5 type-5 candidate   9 finished
// Pat.dir   : -1 the high was swept (bearish CRT)   +1 the low was swept (bullish CRT)
// Pat.typ   : 1..6 the sub type   7 other   8 not completed

// The formations of one timeframe
type PSet
    string       name
    array<Pat>   act
    array<Pat>   fin
    array<label> lbs
    array<int>   nForm
    array<int>   n50
    array<int>   n100
    array<int>   nKL
    float        fHi
    float        fLo
    int          fHiT
    int          fLoT
    int          curT

f_pset(string name) =>
    PSet.new(name, array.new<Pat>(), array.new<Pat>(), array.new<label>(), array.new<int>(9, 0), array.new<int>(9, 0), array.new<int>(9, 0), array.new<int>(9, 0))

f_stSeen(Pat p, int d) =>
    d == -1 ? p.upSeen : p.dnSeen

f_stBack(Pat p, int d) =>
    d == -1 ? p.backDn : p.backUp

f_stBnd(Pat p, int d) =>
    d == -1 ? p.hi : p.lo

f_stFar(Pat p, int d) =>
    d == -1 ? p.lo : p.hi

f_stMid(Pat p) =>
    (p.hi + p.lo) / 2

// Has price x gone at least as far as lvl in the delivery direction (down for d = -1, up for d = +1)?
// Half a tick of tolerance, so a level that is an exact midpoint is not missed by rounding.
f_stReached(float x, float lvl, int d) =>
    d == -1 ? x <= lvl + syminfo.mintick / 2 : x >= lvl - syminfo.mintick / 2

// Label text: "ST1" when finished, "ST1?" while it can still change, "x" when the opposite end was not reached
f_stText(Pat p) =>
    string s = ""
    if p.done
        s := p.typ == 7 ? "ST other" : p.typ == 8 ? "ST x" : "ST" + str.tostring(p.typ) + ((p.typ == 4 or p.typ == 6) and not p.r100 ? " x" : "")
    else if p.stage == 5
        s := "ST5?"
    else if p.stage == 1
        s := p.nInside > 0 ? "ST4?" : p.nSweep >= 2 ? "ST6?" : p.nDeliv == 0 ? "ST1?" : p.nDeliv == 1 ? "ST2?" : "ST?"
    s

// Bearish / bullish colour; grey when the opposite end was not reached. Type 5 takes the colour of the
// continuation (bullish after a sweep of the high).
f_stCol(Pat p) =>
    bool t5 = p.typ == 5 or (not p.done and p.stage == 5)
    t5 ? (p.dir == -1 ? stBullCol : stBearCol) : p.done and not p.r100 ? stFailCol : p.dir == -1 ? stBearCol : stBullCol

f_stTip(string tfName, Pat p) =>
    string nm = p.typ == 7 ? "other" : p.typ == 8 ? "not completed" : "sub type " + str.tostring(p.typ)
    tfName + " CRT " + nm + (p.dir == -1 ? " (high swept)" : " (low swept)") + "\nRange " + f_px(p.hi) + " / " + f_px(p.lo) + "\nInside candles before the sweep: " + str.tostring(p.nInside) + "\nSweep candles: " + str.tostring(p.nSweep) + "\nReached 50%: " + (p.r50 ? "yes" : "no") + "\nReached 100%: " + (p.r100 ? "yes" : "no") + "\nSweep took a key level: " + (p.kl ? "yes" : "no")

// Did the sweep take a key level (a swing of the timeframe one step up, or a level typed in)?
f_stKL(TFState par, int d, float ext, float bnd, int fromT) =>
    [kp, kt] = f_klFind(par, d, ext, bnd, fromT)
    not na(kp) or f_atKL(d, ext, bnd)

// Type 5: the BUY / SELL given during candle 2 turns grey when price did not reach 50% before turning back
f_stGrey(string tfName, int sigDir, int tFrom, int tTo) =>
    if stGrey5 and setups.size() > 0
        for i = 0 to setups.size() - 1
            Setup o = setups.get(i)
            if o.tfName == tfName and o.dir == sigDir and not o.t1hit and not na(o.enT) and o.enT >= tFrom and o.enT < tTo
                if tinyLbl
                    label.set_color(o.lbEn, stGreyCol)
                else
                    label.set_textcolor(o.lbEn, stGreyCol)
                line.set_color(o.lnEn, stGreyCol)
                line.set_color(o.lnSL, stGreyCol)
                line.set_color(o.lnT1, stGreyCol)
                line.set_color(o.lnT2, stGreyCol)
                label.set_textcolor(o.lbSL, stGreyCol)
                label.set_textcolor(o.lbT1, stGreyCol)
                label.set_textcolor(o.lbT2, stGreyCol)
                o.note := "(type 5: price turned back before 50%)"
                o.refreshEntry()
    true

// A formation is finished: it is counted, kept for the HTF candle labels and, with the history on,
// labelled on the chart at the swing point of its sweep
method finish(PSet ps, Pat p, int typ, bool ok100, bool hist) =>
    p.typ   := typ
    p.endT  := ps.curT
    p.done  := true
    p.dead  := true
    p.stage := 9
    p.r100  := ok100
    if ok100
        p.r50 := true
    ps.nForm.set(typ, ps.nForm.get(typ) + 1)
    if p.r50
        ps.n50.set(typ, ps.n50.get(typ) + 1)
    if p.r100
        ps.n100.set(typ, ps.n100.get(typ) + 1)
    if p.kl
        ps.nKL.set(typ, ps.nKL.get(typ) + 1)
    ps.fin.push(p)
    if ps.fin.size() > 40
        ps.fin.shift()
    if hist
        ps.lbs.push(label.new(p.extT, p.ext, ps.name + " " + f_stText(p), xloc = xloc.bar_time, color = NOBG, style = p.dir == -1 ? label.style_label_lower_left : label.style_label_upper_left, textcolor = f_stCol(p), size = stTxt, tooltip = f_stTip(ps.name, p)))
        if ps.lbs.size() > stKeep
            label.delete(ps.lbs.shift())
    true

// A higher-timeframe candle X has closed: what it did to formation p.
// Returns -1 / +1 when p became an active formation in that direction on this candle, else 0.
method onClose(PSet ps, Pat p, HC X, TFState par, bool busyDn, bool busyUp, bool hist) =>
    int promoted = 0
    p.age += 1
    bool up = X.h > p.hi
    bool dn = X.l < p.lo
    if p.stage == 0
        if not up and not dn
            // an inside candle
            p.nInside += 1
            if p.age > 8
                p.dead := true
        else if up and dn
            p.dead := true
        else
            int   d      = up ? -1 : 1
            float xe     = d == -1 ? X.h : X.l
            float bnd    = f_stBnd(p, d)
            bool  inside = d == -1 ? X.c <= bnd : X.c >= bnd
            if d == -1 ? busyDn : busyUp
                p.dead := true
            else if inside or f_stBack(p, d)
                float af = d == -1 ? p.minAfterUp : p.maxAfterDn
                p.dir    := d
                p.nSweep := 1
                p.nDeliv := 0
                p.swT    := X.t
                p.ext    := xe
                p.extT   := d == -1 ? ps.fHiT : ps.fLoT
                p.r50    := f_stSeen(p, d) and not na(af) and f_stReached(af, f_stMid(p), d)
                p.kl     := f_stKL(par, d, xe, bnd, X.t)
                promoted := d
                if inside
                    // swept and closed back inside the range
                    p.stage := 1
                else
                    // closed beyond the swept end after an entry-timeframe close back inside: type 5 candidate
                    p.stage := 5
                    p.c2Ext := xe
                    f_stGrey(ps.name, d, X.t, X.tc)
            else
                // a plain close beyond the range: not a formation
                p.dead := true
    else if p.stage == 1
        int   d   = p.dir
        float bnd = f_stBnd(p, d)
        float xe  = d == -1 ? X.h : X.l
        if d == -1 ? X.c > bnd : X.c < bnd
            // closed beyond the swept end: the opposite end was not reached
            ps.finish(p, p.nInside > 0 ? 4 : p.nSweep >= 2 ? 6 : 8, false, hist)
        else if d == -1 ? X.h > bnd : X.l < bnd
            // another sweep that closed back inside
            p.nSweep += 1
            p.nDeliv := 0
            if not p.kl
                p.kl := f_stKL(par, d, xe, bnd, X.t)
            if d == -1 ? xe > p.ext : xe < p.ext
                p.ext  := xe
                p.extT := d == -1 ? ps.fHiT : ps.fLoT
                p.swT  := X.t
        else
            p.nDeliv += 1
            if p.nDeliv >= stMaxAge
                ps.finish(p, p.nInside > 0 ? 4 : p.nSweep >= 2 ? 6 : 8, false, hist)
    else if p.stage == 5
        p.nDeliv += 1
        bool backIn = p.dir == -1 ? X.c < p.hi : X.c > p.lo
        if backIn or p.nDeliv >= 3
            ps.finish(p, 7, false, hist)
    // what the candle that has just closed did is now used up
    p.upSeen     := false
    p.dnSeen     := false
    p.backDn     := false
    p.backUp     := false
    p.minAfterUp := na
    p.maxAfterDn := na
    promoted

// The chart bar inside the candle that is forming: side d of formation p.
// Returns true when a candle-1 range was swept and delivered by this one candle (sub type 3 or 4).
method live(PSet ps, Pat p, int d, bool busy, HC fc, TFState par, bool hist) =>
    bool  same = false
    float bnd  = f_stBnd(p, d)
    float far  = f_stFar(p, d)
    float px   = d == -1 ? low : high
    bool  seen = f_stSeen(p, d)
    if p.stage == 0
        if seen
            if d == -1
                p.minAfterUp := na(p.minAfterUp) ? low : math.min(p.minAfterUp, low)
            else
                p.maxAfterDn := na(p.maxAfterDn) ? high : math.max(p.maxAfterDn, high)
            if f_stReached(px, far, d) and not busy
                p.dir    := d
                p.nSweep := 1
                p.swT    := fc.t
                p.ext    := d == -1 ? ps.fHi : ps.fLo
                p.extT   := d == -1 ? ps.fHiT : ps.fLoT
                p.kl     := f_stKL(par, d, p.ext, bnd, fc.t)
                ps.finish(p, p.nInside > 0 ? 4 : 3, true, hist)
                same := true
    else if p.stage == 1 and p.dir == d
        if f_stReached(px, f_stMid(p), d)
            p.r50 := true
        if f_stReached(px, far, d)
            // the opposite end is reached: the formation is complete
            int sweeps = p.nSweep + (seen ? 1 : 0)
            int deliv  = seen ? 0 : p.nDeliv + 1
            ps.finish(p, p.nInside > 0 ? 4 : sweeps >= 2 ? 6 : deliv == 1 ? 1 : deliv == 2 ? 2 : 7, true, hist)
    else if p.stage == 5 and p.dir == d
        if d == -1 ? high > p.c2Ext : low < p.c2Ext
            // the continuation: price trades beyond candle 2
            ps.finish(p, 5, false, hist)
    // has the forming candle traded beyond this end of the range? (used from the next bar on)
    if not p.done
        if d == -1 and high > p.hi
            p.upSeen := true
        if d == 1 and low < p.lo
            p.dnSeen := true
    same

method compact(PSet ps) =>
    int i = ps.act.size() - 1
    while i >= 0
        Pat p = ps.act.get(i)
        if p.dead
            ps.act.remove(i)
        i -= 1
    true

method busy(PSet ps, int d) =>
    bool res = false
    if ps.act.size() > 0
        for i = 0 to ps.act.size() - 1
            Pat p = ps.act.get(i)
            if not p.dead and (p.stage == 1 or p.stage == 5) and p.dir == d
                res := true
                break
    res

// Runs on every chart bar for one timeframe, after the candles of that timeframe have been updated
method step(PSet ps, HSet hs, TFState st, TFState par, bool on, bool isNew, bool hist) =>
    if on and hs.cs.size() > 0
        // A. an entry-timeframe candle has closed: did it close back inside a range whose end was traded through?
        if st.eNew and ps.act.size() > 0
            for i = 0 to ps.act.size() - 1
                Pat p = ps.act.get(i)
                if p.upSeen and st.ec < p.hi and st.ec > p.lo
                    p.backDn := true
                if p.dnSeen and st.ec > p.lo and st.ec < p.hi
                    p.backUp := true
        // B. a higher-timeframe candle has closed
        int n = hs.cs.size()
        if isNew and n >= 2
            HC   X      = hs.cs.get(n - 2)
            bool busyDn = ps.busy(-1)
            bool busyUp = ps.busy(1)
            if ps.act.size() > 0
                // oldest candle 1 first: when one candle sweeps several ranges, the oldest one becomes the formation
                for i = 0 to ps.act.size() - 1
                    Pat p = ps.act.get(i)
                    int pr = ps.onClose(p, X, par, busyDn, busyUp, hist)
                    if pr == -1
                        busyDn := true
                    if pr == 1
                        busyUp := true
            ps.compact()
            // the candle that has just closed is a new candle 1
            ps.act.push(Pat.new(X.t, X.h, X.l))
            if ps.act.size() > 24
                ps.act.shift()
        // C. the chart bar inside the forming candle
        HC fcN = hs.cs.last()
        ps.curT := fcN.t
        if isNew or na(ps.fHi)
            ps.fHi  := high
            ps.fLo  := low
            ps.fHiT := time
            ps.fLoT := time
        else
            if high > ps.fHi
                ps.fHi  := high
                ps.fHiT := time
            if low < ps.fLo
                ps.fLo  := low
                ps.fLoT := time
        if ps.act.size() > 0
            HC   fc     = hs.cs.last()
            bool busyDn = ps.busy(-1)
            bool busyUp = ps.busy(1)
            bool sameDn = false
            bool sameUp = false
            for i = 0 to ps.act.size() - 1
                Pat p = ps.act.get(i)
                if not p.dead
                    if ps.live(p, -1, busyDn or sameDn, fc, par, hist)
                        sameDn := true
                if not p.dead
                    if ps.live(p, 1, busyUp or sameUp, fc, par, hist)
                        sameUp := true
            // one candle, one formation: the other waiting ranges swept by the same candle are dropped
            if sameDn or sameUp
                for i = 0 to ps.act.size() - 1
                    Pat p = ps.act.get(i)
                    if not p.dead and p.stage == 0 and ((sameDn and p.upSeen) or (sameUp and p.dnSeen))
                        p.dead := true
            ps.compact()
    true

// Draws one timeframe, starting at bar index x0. w = candle width in bars (even), gap = bars between candles,
// want = number of candles to show.
// Returns the bar index of the right edge of the last candle (or x0 when nothing was drawn).
f_hcDraw(HSet hs, PSet ps, int x0, int w, int gap, int want) =>
    int xEnd = x0
    int n    = hs.cs.size()
    int cnt  = math.min(want, n)
    if cnt > 0 and x0 + cnt * (w + gap) < bar_index + 490
        color noBg  = NOBG
        int   first = n - cnt
        xEnd := x0 + cnt * (w + gap) - gap
        HC    f0  = hs.cs.get(first)
        float top = f0.h
        float bot = f0.l
        for j = first to n - 1
            HC k = hs.cs.get(j)
            top := math.max(top, k.h)
            bot := math.min(bot, k.l)
        // room between the wicks and the texts above / below the set, so the sub-type labels on the wicks stay clear
        float padY = showST ? math.max((top - bot) * 0.08, syminfo.mintick * 5) : 0.0
        // ── Fair Value Gaps and Inverse Fair Value Gaps of these candles (the third candle must be closed).
        //    The box covers the three candles of the gap; a dotted line marks its midpoint.
        if fvgOnHtf and (showFVG or showIFVG) and n - 2 >= first + 2
            float minGap = fvgMinTick * syminfo.mintick
            for i = first + 2 to n - 2
                HC    a   = hs.cs.get(i - 2)
                HC    c3  = hs.cs.get(i)
                int   dir = 0
                float gt  = na
                float gb  = na
                if c3.l > a.h
                    dir := 1
                    gt  := c3.l
                    gb  := a.h
                else if c3.h < a.l
                    dir := -1
                    gt  := a.l
                    gb  := c3.h
                if dir != 0 and gt - gb >= minGap
                    // 1 = FVG, 2 = iFVG (a later candle closed through the gap), 3 = gone (closed back through)
                    int state = 1
                    if i + 1 <= n - 2
                        for q = i + 1 to n - 2
                            HC z = hs.cs.get(q)
                            if state == 1
                                if dir == 1 ? z.c < gb : z.c > gt
                                    state := 2
                            else if state == 2
                                if dir == 1 ? z.c > gt : z.c < gb
                                    state := 3
                    int  xl   = x0 + (i - 2 - first) * (w + gap)
                    int  xr   = x0 + (i - first) * (w + gap) + w
                    bool isF  = state == 1 and showFVG
                    bool isI  = state == 2 and showIFVG
                    if isF or isI
                        color c = isF ? (dir == 1 ? fvgBullCol : fvgBearCol) : (dir == 1 ? ifvBearCol : ifvBullCol)
                        hcBoxes.push(box.new(xl, gt, xr, gb, border_color = c, border_width = isF ? fvgWidth : ifvWidth, border_style = isF ? fvgStyle : ifvStyle, bgcolor = color.new(c, isF ? fvgFill : ifvFill)))
                        if showFvgMid
                            hcLines.push(line.new(xl, (gt + gb) / 2, xr, (gt + gb) / 2, color = c, style = fvgMidStyle, width = fvgMidWidth))
        // ── the candles
        for j = first to n - 1
            HC   k  = hs.cs.get(j)
            int  xl = x0 + (j - first) * (w + gap)
            int  xm = xl + int(w / 2)
            bool up = k.c >= k.o
            hcLines.push(line.new(xm, k.h, xm, k.l, color = hcWickCol, width = 1))
            hcBoxes.push(box.new(xl, math.max(k.o, k.c), xl + w, math.min(k.o, k.c), border_color = hcBorderCol, border_width = 1, bgcolor = up ? hcBullCol : hcBearCol))
            if hcShowTime
                // every second label one row lower, so neighbours do not run into each other
                string tt = (hcTwoRows and (j - first) % 2 == 1 ? "\n" : "") + f_hcTime(hs.name, k.t, k.tc)
                hcLabs.push(label.new(xm, bot - padY, tt, color = noBg, style = label.style_label_up, textcolor = hcTxtCol, size = hcTxt))
        // ── CRT sub type: a formation is a group of candles, so it is shown as one dashed outline around its
        //    candles (candle 1 to the candle that completed it) with the sub type at the top. The formation that
        //    is developing comes first, then the latest completed ones. Outlines never overlap.
        if showST
            array<bool> used  = array.new<bool>(cnt, false)
            int         drawn = 0
            int         nAct  = ps.act.size()
            int         nFin  = ps.fin.size()
            HC          fcS   = hs.cs.get(n - 1)
            if 2 * nAct + nFin > 0
                for q = 0 to 2 * nAct + nFin - 1
                    if drawn < stBoxMax
                        Pat    p    = na
                        string txt  = ""
                        color  tc   = stBearCol
                        int    endT = fcS.t
                        if q < nAct
                            // developing: swept, waiting for the delivery (newest candle 1 first)
                            p := ps.act.get(nAct - 1 - q)
                            if p.stage == 1 or p.stage == 5
                                txt := f_stText(p)
                                tc  := f_stCol(p)
                        else if q < 2 * nAct
                            // the forming candle has swept a range and is back inside it right now
                            p := ps.act.get(q - nAct)
                            if p.stage == 0 and close < p.hi and close > p.lo and ((p.upSeen and not ps.busy(-1)) or (p.dnSeen and not ps.busy(1)))
                                txt := p.nInside > 0 ? "ST4?" : "ST1?"
                                tc  := p.upSeen ? stBearCol : stBullCol
                        else
                            // completed (newest first); a type 5 is complete when the continuation is confirmed
                            p := ps.fin.get(nFin - 1 - (q - 2 * nAct))
                            if p.typ >= 1 and p.typ <= 6 and (p.typ == 5 or p.r100)
                                txt  := f_stText(p)
                                tc   := f_stCol(p)
                                endT := p.endT
                        if txt != ""
                            int js = na
                            int je = na
                            for j = first to n - 1
                                HC k = hs.cs.get(j)
                                if k.t == p.c1T
                                    js := j
                                if k.t == endT
                                    je := j
                            if not na(js) and not na(je) and je >= js
                                bool  free = true
                                float bh   = na
                                float bl   = na
                                for j = js to je
                                    HC k = hs.cs.get(j)
                                    bh := na(bh) ? k.h : math.max(bh, k.h)
                                    bl := na(bl) ? k.l : math.min(bl, k.l)
                                    if used.get(j - first)
                                        free := false
                                if free
                                    for j = js to je
                                        used.set(j - first, true)
                                    hcBoxes.push(box.new(x0 + (js - first) * (w + gap) - 1, bh + padY, x0 + (je - first) * (w + gap) + w + 1, bl - padY / 2, border_color = stBoxCol, border_width = stBoxWidth, border_style = stBoxStyle, bgcolor = NOBG, text = txt, text_color = tc, text_size = hcTxt, text_halign = text.align_center, text_valign = text.align_top))
                                    drawn += 1
        // ── sweeps, newest first: a candle traded beyond the high / low of an earlier candle that nobody had
        //    taken yet, and closed (or, for the forming candle, is now) back inside it
        if hcShowSweep and cnt >= 2
            int shown = 0
            for j = n - 1 to first + 1
                HC    k     = hs.cs.get(j)
                int   xm    = x0 + (j - first) * (w + gap) + int(w / 2)
                float runH  = na
                float runL  = na
                bool  doneH = false
                bool  doneL = false
                for i = j - 1 to first
                    HC  p  = hs.cs.get(i)
                    int xp = x0 + (i - first) * (w + gap) + int(w / 2)
                    if not doneH
                        if (na(runH) or p.h > runH) and k.h > p.h and k.c < p.h
                            if shown < hcSwMax
                                hcLines.push(line.new(xp, p.h, xm, p.h, color = hcSwBearCol, style = hcSwStyle, width = hcSwWidth))
                                shown += 1
                            doneH := true
                        runH := na(runH) ? p.h : math.max(runH, p.h)
                        if runH >= k.h
                            doneH := true
                    if not doneL
                        if (na(runL) or p.l < runL) and k.l < p.l and k.c > p.l
                            if shown < hcSwMax
                                hcLines.push(line.new(xp, p.l, xm, p.l, color = hcSwBullCol, style = hcSwStyle, width = hcSwWidth))
                                shown += 1
                            doneL := true
                        runL := na(runL) ? p.l : math.min(runL, p.l)
                        if runL <= k.l
                            doneL := true
        // ── the open of the forming candle: a line from the chart bar where it opened to the candle
        HC fc = hs.cs.get(n - 1)
        if hcShowOpen
            hcLines.push(line.new(math.max(fc.bi, bar_index - 4500), fc.o, xEnd, fc.o, color = hcOpenCol, style = hcOpenStyle, width = hcOpenWidth))
        // ── timeframe, trend-bias arrow and time left above the candles
        if hcShowTf or hcShowLeft or showBiasArrow
            int    b   = f_bias(hs)
            string l1  = (hcShowTf ? hs.name : "") + (hcShowTf and showBiasArrow ? " " : "") + (showBiasArrow ? f_arrow(b) : "")
            string txt = l1 + (l1 != "" and hcShowLeft ? "\n" : "") + (hcShowLeft ? "(" + f_hcLeft(fc.tc) + ")" : "")
            color  tc  = showBiasArrow and b == 1 ? biasUpCol : showBiasArrow and b == -1 ? biasDnCol : hcTxtCol
            hcLabs.push(label.new(int((x0 + xEnd) / 2), top + padY, txt, color = noBg, style = label.style_label_down, textcolor = tc, size = hcTxt))
    xEnd

// The candles of every timeframe above the chart's are kept (the trend bias needs them);
// a set is drawn when it is switched on
bool hcFeed1H = timeframe.in_seconds("60") > chartSec
bool hcFeed4H = timeframe.in_seconds("240") > chartSec
bool hcFeedD  = timeframe.in_seconds("D") > chartSec
bool hcFeedW  = timeframe.in_seconds("W") > chartSec
bool hcFeedM  = timeframe.in_seconds("M") > chartSec
// lowest timeframe first: 1H, 4H, 1D, 1W, 1M
array<bool> hcFeedA = array.from(hcFeed1H, hcFeed4H, hcFeedD, hcFeedW, hcFeedM)
array<bool> hcSelA  = array.from(hc1H, hc4H, hcD, hcW, hcM)
array<bool> inUpA   = array.from(in1H, in4H, inD, inW, inM)
array<bool> newA    = array.from(new1H, new4H, newD, newW, newM)
array<int>  tnA     = array.from(tn1H, tn4H, tnD, tnW, tnM)
array<int>  tcnA    = array.from(tcn1H, tcn4H, tcnD, tcnW, tcnM)
array<int>  hcNA    = array.from(hcN1H, hcN4H, hcND, hcNW, hcNM)

var array<HSet> hsAll = array.new<HSet>()
var array<PSet> psAll = array.new<PSet>()
if barstate.isfirst
    hsAll.push(HSet.new("1H", array.new<HC>()))
    hsAll.push(HSet.new("4H", array.new<HC>()))
    hsAll.push(HSet.new("1D", array.new<HC>()))
    hsAll.push(HSet.new("1W", array.new<HC>()))
    hsAll.push(HSet.new("1M", array.new<HC>()))
    psAll.push(f_pset("1H"))
    psAll.push(f_pset("4H"))
    psAll.push(f_pset("1D"))
    psAll.push(f_pset("1W"))
    psAll.push(f_pset("1M"))

// The candles of each timeframe, then its CRT sub types. The counts cover the whole loaded history.
bool   stOn   = showST or showSTHist or showSTTable
string fvgMsg = ""
for i = 0 to 4
    HSet   hsI = hsAll.get(i)
    PSet   psI = psAll.get(i)
    string fm  = hsI.feed(hcFeedA.get(i), newA.get(i), tnA.get(i), tcnA.get(i))
    if hcSelA.get(i)
        fvgMsg += fm
    psI.step(hsI, tfUp.get(i), tfPar.get(i), stOn and hcFeedA.get(i), newA.get(i), showSTHist and inUpA.get(i))

// Alert: a new Fair Value Gap on one of the HTF candle timeframes that are switched on
if alFVG and fvgMsg != ""
    alert(fvgMsg, alert.freq_once_per_bar)

if barstate.islast
    // redraw from scratch on every update of the last bar
    while hcBoxes.size() > 0
        box.delete(hcBoxes.pop())
    while hcLines.size() > 0
        line.delete(hcLines.pop())
    while hcLabs.size() > 0
        label.delete(hcLabs.pop())
    if showHtf
        int w   = hcSizeIn == "Tiny" ? 2 : hcSizeIn == "Small" ? 4 : hcSizeIn == "Medium" ? 6 : hcSizeIn == "Large" ? 8 : 12
        int gap = hcSizeIn == "Tiny" ? 1 : hcSizeIn == "Small" ? 2 : hcSizeIn == "Medium" ? 2 : hcSizeIn == "Large" ? 3 : 4
        int x   = bar_index + hcOffset
        for i = 0 to 4
            if hcSelA.get(i) and hcFeedA.get(i)
                x := f_hcDraw(hsAll.get(i), psAll.get(i), x, w, gap, hcNA.get(i) > 0 ? hcNA.get(i) : hcCount) + hcSetGap

// ═════════════════════════════════════════════════════════════════════════════
//  TREND BIAS TABLE
// ═════════════════════════════════════════════════════════════════════════════
// One cell per HTF candle timeframe that is switched on in group 10 and is higher than the chart timeframe
f_biasCell(table t, int col, HSet hs) =>
    int b = f_bias(hs)
    table.cell(t, col, 0, hs.name + " " + f_arrow(b), text_color = color.white, text_size = biasTxt, bgcolor = b == 1 ? biasUpCol : b == -1 ? biasDnCol : color.new(color.gray, 30), tooltip = hs.name + " trend bias: " + (b == 1 ? "up" : b == -1 ? "down" : "not known yet") + "\nClose above the range = up, close below = down, wick above with a close back inside = down, wick below with a close back inside = up. Inside bars keep the bias.")
    true

var table biasTb = table.new(f_pos(biasPosIn), 6, 1, bgcolor = color.new(#131722, 15), frame_color = color.new(color.gray, 40), frame_width = 1, border_color = color.new(color.gray, 70), border_width = 1)

if barstate.islast
    table.clear(biasTb, 0, 0, 5, 0)
    if showBiasTable
        int col = 1
        for i = 0 to 4
            if hcSelA.get(i) and hcFeedA.get(i)
                f_biasCell(biasTb, col, hsAll.get(i))
                col += 1
        if col > 1
            table.cell(biasTb, 0, 0, "Trend Bias", text_color = color.silver, text_size = biasTxt)

// ═════════════════════════════════════════════════════════════════════════════
//  CRT SUB TYPE COUNT TABLE
// ═════════════════════════════════════════════════════════════════════════════
// One column per CRT timeframe that is switched on and higher than the chart timeframe. Each cell shows
// how many formations of that sub type were found on the loaded history, how many of them reached 50% of
// the range and how many reached 100%.
f_stCol_(table t, int col, PSet ps) =>
    table.cell(t, col, 0, ps.name, text_color = color.silver, text_size = stTbTxt)
    for r = 1 to 8
        int nf = ps.nForm.get(r)
        table.cell(t, col, r, nf == 0 ? "-" : str.tostring(nf) + " / " + str.tostring(ps.n50.get(r)) + " / " + str.tostring(ps.n100.get(r)), text_color = color.white, text_size = stTbTxt, tooltip = ps.name + ": " + str.tostring(nf) + " formed, " + str.tostring(ps.n50.get(r)) + " reached 50%, " + str.tostring(ps.n100.get(r)) + " reached 100%, " + str.tostring(ps.nKL.get(r)) + " with a key level taken by the sweep")
    true

var table stTb = table.new(f_pos(stTbPosIn), 6, 9, bgcolor = color.new(#131722, 15), frame_color = color.new(color.gray, 40), frame_width = 1, border_color = color.new(color.gray, 70), border_width = 1)

if barstate.islast
    table.clear(stTb, 0, 0, 5, 8)
    if showSTTable
        int col = 1
        for i = 0 to 4
            if inUpA.get(i) and hcFeedA.get(i)
                f_stCol_(stTb, col, psAll.get(i))
                col += 1
        if col > 1
            f_cell(stTb, 0, 0, "CRT sub type\nformed / 50% / 100%", color.silver, stTbTxt, "")
            array<string> stNm  = array.from("ST1", "ST2", "ST3", "ST4", "ST5", "ST6", "Other", "Not completed")
            array<string> stTip = array.from("Candle 2 sweeps and closes back inside; candle 3 reaches the opposite end.", "As ST1, but the opposite end is reached on candle 4.", "Candle 2 sweeps and reaches the opposite end by itself.", "Inside candle(s) first, then the sweep and the delivery.", "The reversal failed and price continued. It never reaches 100% by definition.", "Two or more sweep candles before the delivery.", "The opposite end was reached, but the sequence fits none of the six.", "A single sweep that never reached the opposite end.")
            for r = 0 to 7
                f_cell(stTb, 0, r + 1, stNm.get(r), color.white, stTbTxt, stTip.get(r))

// ═════════════════════════════════════════════════════════════════════════════
//  DAY AND WEEK DIVIDERS
// ═════════════════════════════════════════════════════════════════════════════
// A vertical line on the first bar of each trading day / week of the symbol, and the letter of the day
// (M T W T F S S) at the top of the chart
var array<line> divLines = array.new<line>()
bool divDayOk  = showDayDiv and hcFeedD
bool divWeekOk = showWeekDiv and hcFeedW
bool divNewD   = hcFeedD and newD
bool divNewW   = hcFeedW and newW
if divWeekOk and divNewW
    divLines.push(line.new(time, low, time, high + syminfo.mintick, xloc = xloc.bar_time, extend = extend.both, color = weekDivCol, style = weekDivStyle, width = weekDivWidth))
else if divDayOk and divNewD
    divLines.push(line.new(time, low, time, high + syminfo.mintick, xloc = xloc.bar_time, extend = extend.both, color = dayDivCol, style = dayDivStyle, width = dayDivWidth))
while divLines.size() > divMax
    line.delete(divLines.shift())

// the trading day that is starting (the day its daily candle closes on, read on the clock of group 7)
int  divDow = dayofweek(tcnD - 1, tzIn)
bool divLet = divDayOk and showDayLetter and divNewD
plotchar(divLet and divDow == dayofweek.monday, "Monday", "M", location.top, color = dayDivTxtCol, size = size.tiny, editable = false, display = display.pane)
plotchar(divLet and (divDow == dayofweek.tuesday or divDow == dayofweek.thursday), "Tuesday / Thursday", "T", location.top, color = dayDivTxtCol, size = size.tiny, editable = false, display = display.pane)
plotchar(divLet and divDow == dayofweek.wednesday, "Wednesday", "W", location.top, color = dayDivTxtCol, size = size.tiny, editable = false, display = display.pane)
plotchar(divLet and divDow == dayofweek.friday, "Friday", "F", location.top, color = dayDivTxtCol, size = size.tiny, editable = false, display = display.pane)
plotchar(divLet and (divDow == dayofweek.saturday or divDow == dayofweek.sunday), "Saturday / Sunday", "S", location.top, color = dayDivTxtCol, size = size.tiny, editable = false, display = display.pane)

// ═════════════════════════════════════════════════════════════════════════════
//  CISD AND MSS  (chart timeframe)
// ═════════════════════════════════════════════════════════════════════════════
// CISD (Change in State of Delivery)
//   bullish: a candle closes above the open of the latest run of down-close candles
//   bearish: a candle closes below the open of the latest run of up-close candles
//   Only a change is marked: a bullish CISD follows a bearish one and the reverse.
// MSS (Market Structure Shift)
//   bullish: a candle closes above the last swing high while the structure was bearish
//   bearish: a candle closes below the last swing low while the structure was bullish
//   A break in the direction the structure already had is not marked.
var array<line>  cisdLines = array.new<line>()
var array<label> cisdLabs  = array.new<label>()
var array<line>  cisdLeads = array.new<line>()
var array<line>  mssLeads  = array.new<line>()
var array<line>  mssLines  = array.new<line>()
var array<label> mssLabs   = array.new<label>()

// Is the candle 'back' bars ago clear of the price level (the level is above its high or below its low)?
f_clearOf(int back, float lvl, float pad) =>
    lvl > high[back] + pad or lvl < low[back] - pad

// Draws one mark: the level line from bar b0 to this bar, and its label. With "clear spot" on, the label
// is moved off the candles: to the nearest place left of the line where the candles do not touch the
// level, or else just right of this bar, and a dotted leader joins it to the line.
f_mark(array<line> lns, array<line> lds, array<label> lbs, int maxN, int b0, float lvl, int dir, string txt, color c, string sty, int wid, float pad) =>
    int    x0   = math.max(b0, bar_index - 4500)
    color  noBg = NOBG
    string tip  = (dir == 1 ? "Bullish " : "Bearish ") + txt + " at " + f_px(lvl)
    line   ld   = na
    label  lb   = na
    lns.push(line.new(x0, lvl, bar_index, lvl, color = c, style = sty, width = wid))
    if cmClear
        // room for the text: this many candles in a row must be clear of the level
        int need  = 4
        int found = na
        int k     = 1
        while k <= cmReach and na(found)
            int xr = x0 - k
            if xr - need + 1 >= 0 and bar_index - (xr - need + 1) < 500
                bool ok = true
                for q = 0 to need - 1
                    if not f_clearOf(bar_index - (xr - q), lvl, pad)
                        ok := false
                        break
                if ok
                    found := xr
            k += 1
        if not na(found)
            ld := line.new(found, lvl, x0, lvl, color = c, style = line.style_dotted, width = 1)
            lb := label.new(found, lvl, txt, color = noBg, style = label.style_label_right, textcolor = c, size = size.tiny, tooltip = tip)
        else
            ld := line.new(bar_index, lvl, bar_index + 3, lvl, color = c, style = line.style_dotted, width = 1)
            lb := label.new(bar_index + 3, lvl, txt, color = noBg, style = label.style_label_left, textcolor = c, size = size.tiny, tooltip = tip)
    else
        lb := label.new(int((x0 + bar_index) / 2), lvl, txt, color = noBg, style = dir == 1 ? label.style_label_up : label.style_label_down, textcolor = c, size = size.tiny, tooltip = tip)
    lds.push(ld)
    lbs.push(lb)
    if lns.size() > maxN
        line.delete(lns.shift())
        line.delete(lds.shift())
        label.delete(lbs.shift())
    true

float cmPad = nz(ta.atr(14), 0) * 0.25

// ── CISD
var float cdDnO  = na
var int   cdDnB  = na
var float cdUpO  = na
var int   cdUpB  = na
var int   cdDir  = 0
string cmMsg = ""
if barstate.isconfirmed
    if not na(cdDnO) and close > cdDnO and cdDir != 1
        cdDir := 1
        if showCISD
            f_mark(cisdLines, cisdLeads, cisdLabs, cisdMax, cdDnB, cdDnO, 1, "CISD", cisdBullCol, cisdStyle, cisdWidth, cmPad)
        if alCISD
            cmMsg += syminfo.ticker + " bullish CISD: close above " + f_px(cdDnO) + "\n"
        cdDnO := na
    else if not na(cdUpO) and close < cdUpO and cdDir != -1
        cdDir := -1
        if showCISD
            f_mark(cisdLines, cisdLeads, cisdLabs, cisdMax, cdUpB, cdUpO, -1, "CISD", cisdBearCol, cisdStyle, cisdWidth, cmPad)
        if alCISD
            cmMsg += syminfo.ticker + " bearish CISD: close below " + f_px(cdUpO) + "\n"
        cdUpO := na
    // the open of the first candle of each new run of down-close / up-close candles
    if close < open and not (close[1] < open[1])
        cdDnO := open
        cdDnB := bar_index
    if close > open and not (close[1] > open[1])
        cdUpO := open
        cdUpB := bar_index

// ── MSS
// A swing high is a candle whose high is above the highs of 'n' candles on each side (a swing low is the
// mirror image). It is known 'n' candles after it formed.
f_swing(int n, bool isHigh) =>
    float v  = isHigh ? high[n] : low[n]
    bool  ok = bar_index >= 2 * n and not na(v)
    if ok
        for q = 0 to 2 * n
            if q != n
                float o = isHigh ? high[q] : low[q]
                if isHigh ? o >= v : o <= v
                    ok := false
                    break
    ok ? v : na

float msPH = f_swing(mssLen, true)
float msPL = f_swing(mssLen, false)
var float msH   = na
var int   msHB  = na
var float msL   = na
var int   msLB  = na
var int   msDir = 0
if not na(msPH)
    msH  := msPH
    msHB := bar_index - mssLen
if not na(msPL)
    msL  := msPL
    msLB := bar_index - mssLen
if barstate.isconfirmed
    if not na(msH) and close > msH
        if msDir != 1
            if showMSS
                f_mark(mssLines, mssLeads, mssLabs, mssMax, msHB, msH, 1, "MSS", mssBullCol, mssStyle, mssWidth, cmPad)
            if alMSS
                cmMsg += syminfo.ticker + " bullish MSS: close above the swing high " + f_px(msH) + "\n"
        msDir := 1
        msH   := na
    else if not na(msL) and close < msL
        if msDir != -1
            if showMSS
                f_mark(mssLines, mssLeads, mssLabs, mssMax, msLB, msL, -1, "MSS", mssBearCol, mssStyle, mssWidth, cmPad)
            if alMSS
                cmMsg += syminfo.ticker + " bearish MSS: close below the swing low " + f_px(msL) + "\n"
        msDir := -1
        msL   := na

if cmMsg != ""
    alert(cmMsg, alert.freq_once_per_bar_close)
